Code402.AgentTools.psm1

# Code402.AgentTools.psm1 — PowerShell client for the hcrb.in paid agent API.
# Origin (canonical): https://hcrb.in — operated by JUANA LIMITED (UK).
#
# Wire format verified live 2026-09-27 against the production surface:
# call: POST {Origin}/v1/tools/{tool}/call body: {"input":{...}}
# free: succeeds within daily free quota (per /status)
# paid: HTTP 402 + PAYMENT-REQUIRED header (base64 JSON, accepts[]) and/or JSON body
# with accepts[], settle.url, "Resend with X-PAYMENT header to settle atomically"
# settle: rebuild EIP-3009 transferWithAuthorization, retry with X-PAYMENT header
# assets: USDC 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 on eip155:8453 (Base),
# EURC 0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42 — quotes list live prices
#
# secp256k1 (EIP-3009 signing) has no built-in .NET primitive. The module takes a
# -SignAuthorization scriptblock so any wallet/backend can supply the signature;
# it never sees your private key unless your signer chooses to hold it.

Set-StrictMode -Version Latest

$script:DefaultOrigin = 'https://hcrb.in'

function Get-AgentApiHealth {
    <#
    .SYNOPSIS
        Probes https://hcrb.in/health — free. Returns version, tool catalogue and live prices.
    #>

    [CmdletBinding()]
    param([string]$Origin = $script:DefaultOrigin)
    Invoke-RestMethod -Uri "$Origin/health" -Method Get
}

function Get-AgentApiStatus {
    <#
    .SYNOPSIS
        Reads https://hcrb.in/status — free-tier quota and service state.
    #>

    [CmdletBinding()]
    param([string]$Origin = $script:DefaultOrigin)
    Invoke-RestMethod -Uri "$Origin/status" -Method Get
}

function Get-AgentToolQuote {
    <#
    .SYNOPSIS
        Probes a paid tool and returns the parsed x402 payment challenge (quote).
        Does NOT pay. The tool name must exist in the /health catalogue.
    .EXAMPLE
        Get-AgentToolQuote -Tool iban-check -ToolInput @{ iban = 'GB29NWBK60161331926819' }
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string]$Tool,
        [hashtable]$ToolInput = @{},
        [string]$Origin = $script:DefaultOrigin
    )
    $uri = "$Origin/v1/tools/$Tool/call"
    $bodyObj = @{ input = $ToolInput }
    try {
        Invoke-WebRequest -Uri $uri -Method Post -ContentType 'application/json' `
            -Body ($bodyObj | ConvertTo-Json -Depth 8 -Compress) | Out-Null
        throw "Tool '$Tool' answered without payment (free tier). No quote to show."
    }
    catch [System.Net.WebException] {
        $resp = $_.Exception.Response
        if (-not $resp -or [int]$resp.StatusCode -ne 402) { throw }

        # Preferred source: PAYMENT-REQUIRED header (base64 JSON, x402 v2 shape).
        $h = $resp.Headers['PAYMENT-REQUIRED']
        if ($h) {
            try { return ([System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($h)) | ConvertFrom-Json) } catch { }
        }
        # Fallback: JSON body (x402 v1 shape with accepts[], settle, amount...).
        $reader = New-Object System.IO.StreamReader($resp.GetResponseStream())
        return ($reader.ReadToEnd() | ConvertFrom-Json)
    }
}

function Invoke-AgentApiCall {
    <#
    .SYNOPSIS
        Calls a paid hcrb.in tool end-to-end over x402:
        request -> 402 quote -> pick asset -> sign EIP-3009 -> retry with X-PAYMENT
        -> answer + receipt.
    .PARAMETER SignAuthorization
        Scriptblock that receives the EIP-3009 authorization fields
        (@{from,to,value,validAfter,validBefore,nonce}) and returns the 0x-prefixed
        signature for transferWithAuthorization(from,to,value,validAfter,validBefore,nonce).
        Keeps the private key in your signer, not in this module.
    .PARAMETER Currency
        Which quote asset to accept ('USDC' default, 'EURC' supported live).
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string]$Tool,
        [hashtable]$ToolInput = @{},
        [Parameter(Mandatory = $true)][scriptblock]$SignAuthorization,
        [string]$Currency = 'USDC',
        [string]$Origin = $script:DefaultOrigin
    )
    $uri = "$Origin/v1/tools/$Tool/call"
    $bodyJson = (@{ input = $ToolInput } | ConvertTo-Json -Depth 8 -Compress)

    $req = [System.Net.HttpWebRequest]::Create($uri)
    $req.Method = 'POST'; $req.ContentType = 'application/json'
    $bytes = [System.Text.Encoding]::UTF8.GetBytes($bodyJson)
    $req.GetRequestStream().Write($bytes, 0, $bytes.Length) | Out-Null

    try { $resp = $req.GetResponse() } catch [System.Net.WebException] { $resp = $_.Exception.Response }
    if (-not $resp) { throw 'No response from origin.' }

    if ([int]$resp.StatusCode -eq 200) {
        $reader = New-Object System.IO.StreamReader($resp.GetResponseStream())
        return ($reader.ReadToEnd() | ConvertFrom-Json)   # free tier answered directly
    }
    if ([int]$resp.StatusCode -ne 402) {
        $reader = New-Object System.IO.StreamReader($resp.GetResponseStream())
        throw "Origin returned $([int]$resp.StatusCode): $($reader.ReadToEnd())"
    }

    # --- parse the quote ---
    $quote = $null
    $h = $resp.Headers['PAYMENT-REQUIRED']
    if ($h) { try { $quote = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($h)) | ConvertFrom-Json } catch { } }
    if (-not $quote) {
        $reader = New-Object System.IO.StreamReader($resp.GetResponseStream())
        $quote = $reader.ReadToEnd() | ConvertFrom-Json
    }
    $accept = @($quote.accepts | Where-Object { $_.extra.currency -eq $Currency })[0]
    if (-not $accept) { throw "No $Currency quote offered. Offered: $(@($quote.accepts | ForEach-Object { $_.extra.currency }) -join ', ')" }

    # --- build EIP-3009 authorization ---
    $now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
    $auth = @{
        from         = $null   # <- your wallet address; the signer block may fill it
        to           = $accept.payTo
        value        = $accept.maxAmountRequired
        validAfter   = $now - 60
        validBefore  = $now + [Math]::Min(300, [int]$accept.maxTimeoutSeconds)
        nonce        = '0x' + ([Guid]::NewGuid().ToString('N'))
    }
    $signature = & $SignAuthorization $auth
    if ($signature -notmatch '^0x[0-9a-fA-F]{130}$') { throw 'Signer must return a 0x-prefixed 65-byte r||s||v signature.' }

    # --- settle: resend identical request with X-PAYMENT (x402 v1 payload) ---
    $payment = @{
        x402Version = 1
        payload     = @{ authorization = $auth; signature = $signature }
        accepted    = $accept
    } | ConvertTo-Json -Depth 8 -Compress
    $xPayment = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($payment))

    $payReq = [System.Net.HttpWebRequest]::Create($uri)
    $payReq.Method = 'POST'; $payReq.ContentType = 'application/json'
    $payReq.Headers['X-PAYMENT'] = $xPayment
    $payReq.GetRequestStream().Write($bytes, 0, $bytes.Length) | Out-Null

    try { $payResp = $payReq.GetResponse() } catch [System.Net.WebException] { $payResp = $_.Exception.Response }
    if (-not $payResp) { throw 'No response on settle retry.' }
    $payReader = New-Object System.IO.StreamReader($payResp.GetResponseStream())
    $result = $payReader.ReadToEnd() | ConvertFrom-Json
    if ([int]$payResp.StatusCode -ne 200) { throw "Settle failed $([int]$payResp.StatusCode): $($result | ConvertTo-Json -Compress -Depth 4)" }

    # Receipt: PAYMENT-RESPONSE header when present; XDR-1 receipt body fields otherwise.
    $receiptHeader = $payResp.Headers['PAYMENT-RESPONSE']
    [pscustomobject]@{
        Tool        = $Tool
        Result      = $result
        Paid        = $accept.maxAmountRequired
        Currency    = $Currency
        PayTo       = $accept.payTo
        TxReceipt   = $result.receipt
        RawReceiptHeader = $receiptHeader
    }
}

Export-ModuleMember -Function Get-AgentApiHealth, Get-AgentApiStatus, Get-AgentToolQuote, Invoke-AgentApiCall