Files

712 lines
31 KiB
TypeScript

// The platform, faked at the network layer. Every request the storefront makes to
// api.hanzo.ai (`/v1/…`, `/.well-known/agent-skills/…`) and to hanzo.id is
// answered here from a small stateful world, so a flow's writes are visible to
// its later reads. Anything the world does not know answers 404 problem+json —
// exactly what the live gateway says for an operation it does not serve.
//
// The shapes are cloud's (hanzo-inc/cloud 039a518e9). A hire is its two steps: a
// 402 with x402 terms, then the payment, whose signature is checked against the
// terms with viem's own EIP-712 recovery — and the org wallet that signs it holds
// a real key, generated per world. As in cloud's jobs.go, a hire sent with an
// attempt is one job however often it is sent: the quote's terms while it is
// quoted or funding, the job once it opened, 422 for another request under it.
// A presented payment takes the quote to funding; a payment given up is refused
// (nonce_replayed). `lose` drops the answer to a hire step after it took effect,
// as a gateway timeout does.
import type { Page, Route } from '@playwright/test'
import { hashMessage, recoverAddress, recoverTypedDataAddress, type Hex } from 'viem'
import { generatePrivateKey, privateKeyToAccount, type PrivateKeyAccount } from 'viem/accounts'
/** An hour ago, so every record falls in the current tax year. */
export const NOW = Math.floor(Date.now() / 1000) - 3600
type Row = Record<string, unknown>
export interface World {
org: string
sub: string
/** How clearance decides: clear in full, clear net of withholding, needs a step, foreign payee (asks where), or not live. */
clearance: 'clear' | 'withheld' | 'needs' | 'foreign' | 'absent'
cleared: Row[]
listings: Row[]
shop: Row[]
jobs: Row[]
/** Every hire request, both steps. */
hires: Row[]
/** The payment presented for each job, by job id: the one that opened it, once it did. */
paid: Record<string, string>
/** How many answers to lose, per hire step, after the step took effect. */
lose: { quote: number; pay: number }
/** How many presented payments the rail gives up instead of holding (x402 nonce_replayed: "sign another"). */
giveUp: number
wallets: Row[]
/** The key behind each wallet the org creates or holds. */
keys: Record<string, PrivateKeyAccount>
apps: Row[]
tax: Row | null
principal: Row | null
seller: Row | null
challenge: { wallet: string; address: string; digest: Hex } | null
installed: string[]
calls: string[]
}
const reputation = { rating: 4.8, reviews: 23, installs: 1402, jobs: { completed: 57, disputed: 1 } }
const none = { rating: null, reviews: 0, installs: 12, jobs: { completed: 0, disputed: 0 } }
/** A wallet of the acting org, with a real key behind it, for the Hanzo L1 unless it names another chain. */
export function wallet(w: World, id = 'wal_acme', name = 'Treasury', chain = 'eip155:36963'): Row {
const key = privateKeyToAccount(generatePrivateKey())
w.keys[id] = key
const made = { id, accountId: 'acct_1', name, custody: 'mpc', chain, address: key.address }
w.wallets.push(made)
return made
}
/** The seller's payout address the x402 terms name. */
const PAY_TO = '0x209693Bc6afc0C5328bA36FaF03C514EF312287C'
/** The token contract cloud names while its operator pins none (CLOUD_X402_ASSET unset): none, the zero address in the domain. */
const ASSET = ''
const NONE = '0x0000000000000000000000000000000000000000'
export function world(over: Partial<World> = {}): World {
return {
org: 'acme',
sub: 'acme/ada',
clearance: 'clear',
listings: [],
shop: [
{
id: 'lst_research',
publisherOrg: 'orbital',
kind: 'agent',
tool: 'deep-research',
title: 'Deep Research Agent',
description: 'Reads the sources, runs the numbers, writes the brief.',
category: 'research',
price: '250',
currency: 'USD',
public: true,
docs: '/docs/agents/deep-research',
createdAt: NOW - 86_400,
updatedAt: NOW - 86_400,
seller: { org: 'orbital', documented: true, reputation },
reputation,
links: {
docs: '/docs/agents/deep-research',
cli: 'hanzo marketplace jobs create --listing lst_research --brief <brief> --amount <usd>',
mcp: { tool: 'marketplace', op: 'post_marketplace_jobs' },
},
},
{
id: 'lst_geocode',
publisherOrg: 'admin',
kind: 'tool',
tool: 'geocode',
title: 'Geocode',
description: 'Address to coordinates, per call.',
category: 'data',
price: '0.0025',
currency: 'USD',
public: true,
createdAt: NOW - 7200,
updatedAt: NOW - 7200,
seller: { org: 'admin', documented: true, reputation: none },
reputation: none,
links: { cli: 'hanzo marketplace install --tool geocode', mcp: { tool: 'marketplace', op: 'post_marketplace_install' } },
},
],
jobs: [],
hires: [],
paid: {},
lose: { quote: 0, pay: 0 },
giveUp: 0,
wallets: [],
keys: {},
apps: [
{
id: 'hanzo/chat',
org: 'hanzo',
name: 'chat',
title: 'Hanzo Chat',
kind: 'site',
origin: 'product',
description: 'Chat with every frontier model.',
url: 'https://hanzo.chat',
repo: 'https://github.com/hanzoai/chat',
forkable: true,
stars: 311,
language: 'TypeScript',
license: 'MIT',
},
],
tax: null,
principal: {
org: 'acme',
identity: { status: 'none', reason: 'No founder has started identity verification.' },
sanctions: { status: 'clear', reason: '' },
wallets: [],
compliance: {
ready: false,
missing: [
{ code: 'identity', who: 'org', where: 'POST /v1/company/kyc', what: 'No founder has started identity verification.', rule: { code: 'identity', reason: '' } },
{ code: 'tax_form', who: 'org', where: 'PUT /v1/tax/profile', what: 'Certify a W-9 (a U.S. person) or a W-8BEN or W-8BEN-E (a foreign person): a payer asks for it before it pays.', rule: { code: 'status_unknown', reason: '' } },
],
},
sources: [],
},
seller: {
org: 'acme',
ready: false,
missing: ['identity', 'tax_form', 'payout'],
identity: 'none',
tax: undefined,
sanctions: 'clear',
sanctionsReason: '',
payout: { bound: false },
credentials: [],
earnings: { year: new Date().getUTCFullYear(), currency: 'USD', gross: '120.005', payments: 3, byRail: { x402: '120.005' } },
received: [],
sources: [],
},
challenge: null,
installed: [],
cleared: [],
calls: [],
...over,
}
}
const SKILLS = {
base_url: 'https://api.hanzo.ai',
skill_count: 61,
skills: [
{ name: 'vector-search', service: 'vector', description: 'Search embeddings by meaning.', path: 'vector-search/SKILL.md' },
...Array.from({ length: 60 }, (_, i) => ({
name: `op_${String(i).padStart(2, '0')}`,
service: 'ops',
description: `Operation ${i}.`,
path: `op_${i}/SKILL.md`,
})),
],
}
const json = (route: Route, status: number, body: unknown, headers: Record<string, string> = {}) =>
route.fulfill({ status, contentType: 'application/json', body: JSON.stringify(body), headers })
const problem = (route: Route, status: number, detail: string, extra: Row = {}) =>
route.fulfill({ status, contentType: 'application/problem+json', body: JSON.stringify({ type: 'about:blank', status, detail, ...extra }) })
const absent = (route: Route, path: string) => problem(route, 404, `no route for ${path}`)
function b64url(v: unknown): string {
return Buffer.from(JSON.stringify(v)).toString('base64url')
}
/** An unsigned JWT: the storefront reads claims; the gateway (mocked) would check the signature. */
export function token(w: World): string {
return `${b64url({ alg: 'RS256', typ: 'JWT' })}.${b64url({
sub: w.sub,
iss: 'https://hanzo.id',
aud: 'hanzo-market',
orgs: [{ org: w.org, role: 'admin' }],
name: 'Ada Lovelace',
email: '[email protected]',
exp: Math.floor(Date.now() / 1000) + 3600,
})}.sig`
}
/** A page of `rows` as `limit` and `offset` ask. */
function paged(url: URL, rows: Row[]): Row[] {
const limit = Number(url.searchParams.get('limit') ?? 48)
const offset = Number(url.searchParams.get('offset') ?? 0)
return rows.slice(offset, offset + limit)
}
const hits = (url: URL, rows: Row[]) => {
const q = (url.searchParams.get('q') ?? '').toLowerCase()
return rows.filter((r) => !q || JSON.stringify(r).toLowerCase().includes(q))
}
function decide(w: World, body: Row): Row {
const rule = { code: 'r', reason: '' }
const amount = Number(body.amount)
const base = {
id: `clr_${w.cleared.length}`,
payer: w.org,
payee: body.payee,
amount: amount.toFixed(2),
category: body.category,
rail: body.rail,
performed: body.performed,
status: 'us',
reporting_obligations: [],
facts_required: [],
rules: [],
decidedAt: NOW,
notice: 'A clearance is a decision on the facts the platform holds now.',
}
const whole = { rail: 'ledger', net: amount.toFixed(2), withheld: '0.00', rule }
if (w.clearance === 'needs')
return {
...base,
allowed: false,
required_before_payment: [
{ code: 'payer_identity', who: 'payer', where: 'POST /v1/company/kyc', what: "The payer's founders complete identity verification: no founder has started.", rule },
],
settlement_methods: [],
withholding: { reason: "Undetermined until the payee's status is documented.", rule },
}
if (w.clearance === 'foreign' && !body.performed)
return {
...base,
status: 'foreign',
allowed: false,
required_before_payment: [],
facts_required: [
{ code: 'performed', blocks: true, rule, question: 'Where is the service performed — or, for rents, where is the property; for royalties, where is it used?' },
],
settlement_methods: [],
withholding: { reason: 'Undetermined until the source of the income is known.', rule },
}
// Pay for work performed in the U.S. is U.S.-source: a foreign payee is withheld 30% (chapter 3).
if (w.clearance === 'foreign' && body.performed === 'US') {
const kept = (amount * 0.3).toFixed(2)
return {
...base,
status: 'foreign',
allowed: true,
required_before_payment: [],
settlement_methods: [{ rail: 'ledger', net: (amount - Number(kept)).toFixed(2), withheld: kept, rule }],
withholding: { chapter: '3', rate: '30', amount: kept, reason: 'Pay for work performed in the United States is U.S.-source income of a foreign person (IRC §1442).', rule },
}
}
if (w.clearance === 'withheld') {
const kept = (amount * 0.24).toFixed(2)
return {
...base,
allowed: true,
required_before_payment: [],
settlement_methods: [{ rail: 'ledger', net: (amount - Number(kept)).toFixed(2), withheld: kept, rule }],
withholding: { rate: '24', amount: kept, reason: 'The payee has not furnished a TIN, so backup withholding applies (IRC §3406).', rule },
}
}
return {
...base,
status: w.clearance === 'foreign' ? 'foreign' : 'us',
allowed: true,
required_before_payment: [],
settlement_methods: [whole],
withholding: { reason: 'Nothing is withheld: the payee furnished its form.', rule },
}
}
/** The x402 terms cloud's 402 names for a job: USDC-style EIP-3009 on the Hanzo chain. */
function terms(id: string, amount: string) {
return {
x402Version: 2,
resource: { url: `job:${id}` },
accepts: [
{
scheme: 'exact',
network: 'eip155:36963',
amount: String(Math.round(Number(amount) * 1e6)),
asset: ASSET,
payTo: PAY_TO,
maxTimeoutSeconds: 300,
extra: { assetTransferMethod: 'eip3009', name: 'USD Coin', version: '2' },
},
],
}
}
/** Whose signature a payment carries, recovered from the terms it answers — or why it is refused. */
async function signer(payment: string): Promise<{ from: string; to: string; value: string; validBefore: number; nonce: string; resource: string } | string> {
const p = JSON.parse(Buffer.from(payment, 'base64').toString()) as {
x402Version: number
resource: { url: string }
accepted: ReturnType<typeof terms>['accepts'][number]
payload: { signature: Hex; authorization: { from: Hex; to: Hex; value: string; validAfter: string; validBefore: string; nonce: Hex } }
}
const a = p.payload.authorization
const got = await recoverTypedDataAddress({
domain: { name: p.accepted.extra.name, version: p.accepted.extra.version, chainId: 36963, verifyingContract: (p.accepted.asset || NONE) as Hex },
types: {
TransferWithAuthorization: [
{ name: 'from', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'validAfter', type: 'uint256' },
{ name: 'validBefore', type: 'uint256' },
{ name: 'nonce', type: 'bytes32' },
],
},
primaryType: 'TransferWithAuthorization',
message: { from: a.from, to: a.to, value: BigInt(a.value), validAfter: BigInt(a.validAfter), validBefore: BigInt(a.validBefore), nonce: a.nonce },
signature: p.payload.signature,
})
if (got.toLowerCase() !== a.from.toLowerCase()) return `invalid_exact_evm_payload_signature: recovered ${got}, claimed ${a.from}`
return { from: a.from, to: a.to, value: a.value, validBefore: Number(a.validBefore), nonce: a.nonce, resource: p.resource.url }
}
const gatewayTimeout = (route: Route) => problem(route, 504, 'upstream request timeout')
/** What a hire asks, as cloud digests it: the whole request but its payment and its attempt (jobs.go asked). */
const asked = (body: Row) => JSON.stringify({ ...body, payment: undefined, attempt: undefined })
/** The nonces the rail gave up: never held again (x402 planeAuthorize). */
const givenUp = new WeakMap<World, Set<string>>()
/** What each job's hire asked (jobs.asked). */
const digests = new WeakMap<Row, string>()
/** How many jobs a world has quoted: ids are never reused, as cloud's random ones are not. */
const quoted = new WeakMap<World, number>()
async function hire(route: Route, w: World, body: Row) {
w.hires.push(body)
const attempt = typeof body.attempt === 'string' ? body.attempt : ''
if (attempt) {
// One attempt, one job (jobs.go hire): the same request answers the job it made.
const j = w.jobs.find((x) => x.buyerOrg === w.org && x.attempt === attempt)
if (j) {
if (digests.get(j) !== asked(body)) return problem(route, 422, `attempt ${attempt} made job ${String(j.id)} with another request: a new hire is a new attempt`)
if (j.status !== 'quoted' && j.status !== 'funding') return json(route, 201, j)
if (!body.payment) {
if (w.lose.quote > 0) return (w.lose.quote--, gatewayTimeout(route))
return ask(route, j)
}
}
}
const l = w.shop.find((x) => x.id === body.listing)
if (!l) return problem(route, 404, 'listing not found')
const d = decide(w, { payee: l.publisherOrg, amount: body.amount, performed: body.performed, category: 'services', rail: 'x402' })
if (!d.allowed) return problem(route, 403, `clearance ${String(d.id)}: this payment cannot clear now`)
if (Number((d.settlement_methods as { withheld: string }[])[0].withheld) > 0)
return problem(route, 409, `clearance ${String(d.id)}: this payment clears only net of withholding`)
const amount = Number(body.amount).toFixed(2)
if (!body.payment) {
// Without an attempt, the same terms asked again: the quote they already have (store.go quoted).
let q = attempt
? undefined
: w.jobs.find(
(j) =>
j.status === 'quoted' &&
!j.attempt &&
j.buyerOrg === w.org &&
j.listing === body.listing &&
j.brief === body.brief &&
j.amount === amount &&
j.deadline === body.deadline &&
(j.performed ?? '') === (body.performed ?? ''),
)
if (!q) {
quoted.set(w, (quoted.get(w) ?? w.jobs.length) + 1)
q = {
id: `job_${quoted.get(w)}`,
listing: body.listing,
title: l.title,
attempt: attempt || undefined,
buyerOrg: w.org,
sellerOrg: l.publisherOrg,
amount,
currency: 'USD',
category: body.category ?? 'service',
status: 'quoted',
brief: body.brief,
performed: body.performed,
deadline: body.deadline,
review: 259_200,
clearance: d.id,
escrow: { rail: 'x402', network: 'eip155:36963', contract: ASSET },
history: [{ status: 'quoted', at: NOW, by: w.org }],
createdAt: NOW,
updatedAt: NOW,
}
digests.set(q, asked(body))
w.jobs.push(q)
}
if (w.lose.quote > 0) return (w.lose.quote--, gatewayTimeout(route))
return ask(route, q)
}
const who = await signer(body.payment as string)
if (typeof who === 'string') return problem(route, 402, `the payment was refused: ${who}`)
const id = who.resource.replace(/^job:/, '')
const j = w.jobs.find((x) => x.id === id && x.buyerOrg === w.org)
if (!j) return problem(route, 404, 'job not found')
if ((j.attempt ?? '') !== attempt) return problem(route, 409, `the payment is for job ${id}, quoted under another attempt`)
if (j.status !== 'quoted' && j.status !== 'funding') {
// The same payment again: the job it opened (jobs.go fund).
return w.paid[id] === body.payment ? json(route, 200, j) : problem(route, 409, `job ${id} is already paid for`)
}
if (j.amount !== amount) return problem(route, 409, `the payment is for job ${id}, at another amount`)
if (who.validBefore < Number(body.deadline) + 18 * 86_400) return problem(route, 402, 'the authorization must stay valid through the deadline')
const gone = givenUp.get(w) ?? new Set<string>()
givenUp.set(w, gone)
if (gone.has(who.nonce.toLowerCase())) return problem(route, 402, 'the payment was refused: this payment was given up: sign another (nonce_replayed)')
// Presented: the quote is funding, and keeps the payment (jobs.go present); a payment before it is given up.
if (w.paid[id] && w.paid[id] !== body.payment) {
const before = await signer(w.paid[id])
if (typeof before !== 'string') gone.add(before.nonce.toLowerCase())
}
w.paid[id] = body.payment as string
if (j.status === 'quoted') Object.assign(j, { status: 'funding', history: [...(j.history as Row[]), { status: 'funding', at: NOW, by: w.org }] })
if (w.giveUp > 0) {
w.giveUp--
gone.add(who.nonce.toLowerCase())
return problem(route, 402, 'the payment was refused: this payment was given up: sign another (nonce_replayed)')
}
Object.assign(j, {
status: 'open',
payer: who.from,
escrow: { rail: 'x402', network: 'eip155:36963', contract: ASSET, payTo: PAY_TO },
history: [...(j.history as Row[]), { status: 'open', at: NOW, by: w.org }],
})
if (w.lose.pay > 0) return (w.lose.pay--, gatewayTimeout(route))
return json(route, 201, j)
}
/** The 402 that names what to sign for a quoted job (jobs.go ask). */
function ask(route: Route, q: Row) {
const t = terms(String(q.id), String(q.amount))
return problem(route, 402, `sign the payment for job:${String(q.id)}`, { paymentRequired: Buffer.from(JSON.stringify(t)).toString('base64'), ...t })
}
async function api(route: Route, w: World) {
const req = route.request()
const url = new URL(req.url())
const path = url.pathname
const method = req.method()
const body = (req.postData() ? JSON.parse(req.postData()!) : {}) as Row
w.calls.push(`${method} ${path}${url.search}`)
// ── catalog sources ──
if (method === 'GET' && path === '/v1/marketplace/shop') {
const all = hits(url, w.shop)
const kind = url.searchParams.get('kind')
const hit = all.filter((l) => !kind || l.kind === kind)
const facets: Record<string, number> = {}
for (const l of all) facets[String(l.kind)] = (facets[String(l.kind)] ?? 0) + 1
return json(route, 200, { listings: paged(url, hit), total: hit.length, facets: { kind: facets, category: {}, price: {}, rating: {} } })
}
const one = path.match(/^\/v1\/marketplace\/shop\/([^/]+)$/)
if (method === 'GET' && one) {
const l = w.shop.find((x) => x.id === decodeURIComponent(one[1]))
return l ? json(route, 200, l) : absent(route, path)
}
if (method === 'GET' && path === '/v1/catalog') {
const org = url.searchParams.get('org')
const hit = hits(url, w.apps).filter((a) => !org || a.org === org)
return json(route, 200, { data: paged(url, hit), total: hit.length, facets: {} })
}
if (method === 'GET' && path === '/.well-known/agent-skills/index.json') return json(route, 200, SKILLS)
if (method === 'GET' && path.startsWith('/.well-known/agent-skills/') && path.endsWith('/SKILL.md'))
return route.fulfill({ status: 200, contentType: 'text/markdown', body: '# vector-search\n\nSearch embeddings by meaning.' })
// Everything below needs a principal.
if (!req.headers().authorization?.startsWith('Bearer ')) return problem(route, 403, 'a validated principal is required')
if (method === 'GET' && path === '/v1/tool/catalog')
return json(route, 200, {
catalog: [
{ id: 'com.stripe_mcp', name: 'stripe', vendor: 'Stripe', title: 'Stripe', description: 'Payments over MCP.', version: '1.2.0', transports: ['http'], featured: true, official: true },
],
total: 1,
})
// ── principal ──
if (method === 'GET' && path === '/v1/principal') return w.principal ? json(route, 200, w.principal) : absent(route, path)
if (method === 'POST' && path === '/v1/company/kyc') {
;(w.principal!.identity as Row).status = 'pending'
return json(route, 200, {
provider: 'persona',
sessions: [{ email: '[email protected]', ref: 'inq_1', verifyUrl: 'https://verify.invalid/ada', status: 'pending' }],
})
}
if (method === 'POST' && path === '/v1/principal/clearance') {
w.cleared.push(body)
if (w.clearance === 'absent') return absent(route, path)
return json(route, 201, decide(w, body))
}
// ── tax ──
if (method === 'GET' && path === '/v1/tax/profile') return w.tax ? json(route, 200, w.tax) : absent(route, path)
if (method === 'PUT' && path === '/v1/tax/profile') {
w.tax = {
form: body.form ?? 'w9',
name: body.name,
businessName: body.businessName,
classification: body.classification,
address: body.address,
tin: '**-***6789',
tinType: body.tinType,
certification: { status: 'none' },
valid: false,
version: 1,
updatedAt: NOW,
}
return json(route, 200, w.tax)
}
if (method === 'POST' && path === '/v1/tax/profile/certify') {
w.tax = { ...w.tax!, certification: { status: 'certified' }, valid: true }
w.principal!.tax = { form: 'w9', usPerson: true, country: 'US', residence: 'US', certified: true, valid: true }
return json(route, 200, w.tax)
}
if (method === 'GET' && path === '/v1/tax/inbox')
return json(route, 200, {
data: [
{
id: 'stm_1',
payer: { org: 'orbital', name: 'Orbital Labs' },
kind: '1099-NEC',
year: Number(url.searchParams.get('year')),
corrected: false,
boxes: [{ box: '1', label: 'Nonemployee compensation', cents: 125_000 }],
backup: { required: false },
furnishedAt: NOW,
},
],
})
// ── wallets ──
if (method === 'GET' && path === '/v1/wallet') return json(route, 200, { wallets: w.wallets })
if (method === 'POST' && path === '/v1/wallet/accounts') return json(route, 201, { id: 'acct_1', name: body.name })
if (method === 'POST' && path === '/v1/wallet') return json(route, 201, wallet(w, 'wal_acme', String(body.name), typeof body.chain === 'string' ? body.chain : ''))
const signing = path.match(/^\/v1\/wallet\/([^/]+)\/sign$/)
if (method === 'POST' && signing) {
const key = w.keys[decodeURIComponent(signing[1])]
if (!key) return problem(route, 404, 'wallet not found')
const signature = await key.sign({ hash: body.digest as Hex })
return json(route, 200, { address: key.address, digest: body.digest, signature, walletId: signing[1] })
}
// ── the seller ──
if (method === 'GET' && path === '/v1/marketplace/seller') return w.seller ? json(route, 200, w.seller) : absent(route, path)
if (method === 'POST' && path === '/v1/marketplace/seller/payout') {
const wal = w.wallets.find((x) => x.id === body.wallet)
if (!wal) return problem(route, 404, 'wallet not found')
const message = `Hanzo marketplace payout wallet\norg: ${w.org}\nwallet: ${String(wal.id)}\naddress: ${String(wal.address)}\nnonce: 5f0e2a91c4d3b7a8\nexpires: ${NOW + 900}`
w.challenge = { wallet: String(wal.id), address: String(wal.address), digest: hashMessage(message) }
return json(route, 200, { wallet: wal.id, address: wal.address, message, digest: w.challenge.digest, expires: NOW + 900 })
}
if (method === 'POST' && path === '/v1/marketplace/seller/payout/verify') {
const c = w.challenge
if (!c) return problem(route, 409, 'no payout challenge is outstanding')
const got = await recoverAddress({ hash: c.digest, signature: body.signature as Hex })
if (got.toLowerCase() !== c.address.toLowerCase()) return problem(route, 403, `the signature is not ${c.address}'s`)
const payout = { wallet: c.wallet, address: c.address, bound: true, boundAt: NOW }
w.seller = { ...w.seller!, payout }
w.challenge = null
return json(route, 200, payout)
}
// ── seller inventory ──
if (method === 'GET' && path === '/v1/agent')
return json(route, 200, { agents: [{ id: 'ag_1', name: 'triage-bot', description: 'Sorts the inbox', status: 'active' }] })
if (method === 'GET' && path === '/v1/tool/skills/authored') return json(route, 200, { skills: [] })
if (method === 'GET' && path === '/v1/tool/mcp/servers') return json(route, 200, { servers: [] })
// ── listings ──
if (method === 'GET' && path === '/v1/marketplace/listings') return json(route, 200, { listings: w.listings })
if (method === 'POST' && path === '/v1/marketplace/listings') {
const made = { id: `lst_${w.listings.length + 1}`, publisherOrg: w.org, currency: 'USD', createdAt: NOW, updatedAt: NOW, recipient: '', description: '', category: '', ref: 'ag_1', ...body }
w.listings.unshift(made)
return json(route, 201, made)
}
if (method === 'POST' && path === '/v1/marketplace/install') {
w.installed.push(String(body.tool))
return json(route, 200, { tool: body.tool, installed: true })
}
// ── jobs ──
if (method === 'GET' && path === '/v1/marketplace/jobs') {
// The buyer sees its quotes and fundings; a seller never does (jobs.go listJobs).
const role = url.searchParams.get('role')
const mine = w.jobs.filter((j) => (role === 'seller' ? j.sellerOrg === w.org && j.status !== 'quoted' && j.status !== 'funding' : j.buyerOrg === w.org))
return json(route, 200, { jobs: mine })
}
if (method === 'POST' && path === '/v1/marketplace/jobs') return hire(route, w, body)
const jobPath = path.match(/^\/v1\/marketplace\/jobs\/([^/]+)(?:\/(accept|decline|cancel|deliver|release|dispute|refund|feedback))?$/)
if (jobPath) {
const j = w.jobs.find((x) => x.id === jobPath[1])
if (!j) return absent(route, path)
if (method === 'GET' && !jobPath[2]) return json(route, 200, j)
if (jobPath[2] === 'feedback') return json(route, 201, { id: 'fb_1', job: j.id, rating: body.rating })
const next = { accept: 'accepted', decline: 'declined', cancel: 'cancelled', deliver: 'delivered', release: 'released', dispute: 'disputed', refund: 'refunded' }[
jobPath[2] as 'accept'
]
j.status = next
j.updatedAt = NOW + 60
;(j.history as unknown[]).push({ status: next, at: NOW + 60, by: w.org })
if (jobPath[2] === 'deliver') j.delivery = { note: body.note, url: body.url, hash: `0x${'ab'.repeat(32)}`, at: NOW + 60 }
if (jobPath[2] === 'dispute') j.dispute = { reason: body.reason, by: w.org, at: NOW + 60 }
return json(route, 200, j)
}
// ── x402 ──
if (method === 'GET' && path === '/v1/x402/settlements')
return json(route, 200, {
settlements: [
{ id: 'x402_1', resource: 'tool:geocode', payer: 'globex', payee: '0xacme', payeeOrg: w.org, amount: '0.0025', network: 'eip155:36963', settledVia: 'ledger', settledAt: NOW },
{ id: 'x402_2', resource: 'tool:geocode', payer: 'initech', payee: '0xacme', payeeOrg: w.org, amount: '0.0025', network: 'eip155:36963', settledVia: 'ledger', settledAt: NOW },
{ id: 'x402_3', resource: 'job:job_3', payer: 'initech', payee: '0xacme', payeeOrg: w.org, amount: '120', network: 'eip155:36963', settledVia: 'ledger', settledAt: NOW },
],
})
return absent(route, path)
}
async function iam(route: Route, w: World, base: string) {
const req = route.request()
const url = new URL(req.url())
const cors = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': '*', 'Access-Control-Allow-Methods': 'GET,POST,OPTIONS' }
if (req.method() === 'OPTIONS') return route.fulfill({ status: 204, headers: cors })
const issuer = 'https://hanzo.id'
if (url.pathname === '/.well-known/openid-configuration')
return json(
route,
200,
{
issuer,
authorization_endpoint: `${issuer}/v1/iam/oauth/authorize`,
token_endpoint: `${issuer}/v1/iam/oauth/token`,
userinfo_endpoint: `${issuer}/v1/iam/oauth/userinfo`,
jwks_uri: `${issuer}/v1/iam/.well-known/jwks`,
end_session_endpoint: `${issuer}/v1/iam/oauth/logout`,
},
cors,
)
// The embedded sign-in's credential call, posted to the page's own origin: this
// browser holds a session, so IAM answers it with a code (its single sign-on branch).
if (url.pathname === '/v1/iam/login') return json(route, 200, { status: 'ok', data: 'test-code' }, cors)
if (url.pathname === '/v1/iam/oauth/revoke' || url.pathname === '/v1/iam/oauth/logout') return json(route, 200, { status: 'ok' }, cors)
if (url.pathname === '/v1/iam/oauth/authorize') {
const back = new URL(url.searchParams.get('redirect_uri') ?? `${base}/auth/callback`)
back.searchParams.set('code', 'test-code')
back.searchParams.set('state', url.searchParams.get('state') ?? '')
return route.fulfill({ status: 302, headers: { location: back.toString() } })
}
if (url.pathname === '/v1/iam/oauth/token')
return json(
route,
200,
{ access_token: token(w), refresh_token: 'refresh', id_token: token(w), token_type: 'Bearer', expires_in: 3600 },
cors,
)
if (url.pathname === '/v1/iam/oauth/userinfo')
return json(route, 200, { sub: w.sub, owner: w.org, name: 'ada', displayName: 'Ada Lovelace', email: '[email protected]' }, cors)
return route.fulfill({ status: 404, headers: cors })
}
/** Serve the platform and the issuer from `w` for this page. */
export async function mock(page: Page, w: World) {
const base = new URL(page.url() === 'about:blank' ? 'http://127.0.0.1' : page.url()).origin
await page.route(/\/v1\//, (route) => {
const u = new URL(route.request().url())
return u.hostname === 'hanzo.id' || u.pathname.startsWith('/v1/iam/') ? iam(route, w, base) : api(route, w)
})
await page.route(/\/\.well-known\/agent-skills\//, (route) => api(route, w))
await page.route(/^https:\/\/hanzo\.id\//, (route) => iam(route, w, base))
}