Skip to content

feat: enforce request-bound payment authorization v2#313

Open
AnkanMisra wants to merge 10 commits into
mainfrom
feat/gateway-payment-authorization-v2
Open

feat: enforce request-bound payment authorization v2#313
AnkanMisra wants to merge 10 commits into
mainfrom
feat/gateway-payment-authorization-v2

Conversation

@AnkanMisra

@AnkanMisra AnkanMisra commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Summary

  • make the gateway issue v2 challenges bound to the trusted audience, HTTP method, encoded resource, content type, exact request bytes, and claimed payer
  • reconstruct the authorization context server-side on signed retries and require the verifier recovered address to match X-402-Payer
  • reject legacy authorization by default in the verifier, browser client, and TypeScript SDK, including custom-adapter downgrade attempts
  • preserve v2 authorization metadata in signed receipts and expose bounded authorization outcome metrics
  • replace credential-dependent E2E execution with a deterministic loopback provider covering success, replay, and one-byte tampering
  • synchronize OpenAPI, environment templates, Compose/Fly configuration, and protocol documentation

Security properties

  • audience comes only from required PAYGATE_AUDIENCE, never request host or forwarded headers
  • cache hits and misses use the same request-bound verification path
  • v1 is rejected by default through MIN_AUTHORIZATION_VERSION=2
  • malformed payer, verifier mismatch, tampered binding, replay, and verifier failure all fail before paid output

Validation

  • cd gateway && go test -count=1 ./... && go vet ./...
  • cd verifier && cargo fmt -- --check && cargo clippy -- -D warnings && cargo test — 51 passed
  • cd sdk/typescript && bun run typecheck && bun test && bun run build — 26 passed, 1 opt-in live test skipped
  • cd web && bun run lint && bun run typecheck && bun run test:unit && bun run build — 76 passed
  • VERIFIER_NONCE_STORE=memory ./run_e2e.sh — 4 passed
  • YAML parsing, shell syntax, diff checks, and an exhaustive security diff review — passed with no diff-introduced findings

Follow-up to #306.

Note

Enforce request-bound payment authorization v2 across gateway, verifier, and SDK

  • The gateway now builds a canonical v2 PaymentContext server-side, binding audience, method, resource, contentType, and a SHA-256 requestHash to every payment challenge and verification request.
  • The verifier rejects requests below MIN_AUTHORIZATION_VERSION (default 2) with HTTP 400 and error code authorization_version_too_old; the gateway requires a valid X-402-Payer header and rejects signer mismatches with 403 signer_mismatch.
  • The TypeScript SDK's isPaymentContext guard and validatePaymentContextForRequest now reject any context that lacks v2 binding fields, replacing the previous no-op for legacy contexts with a thrown PaygateSdkError.
  • Receipts, smoke tests, and E2E tests are updated to sign with a v2 PaymentAuthorization EIP-712 type that includes payer and request-binding fields; CI now runs deterministically without an external OpenRouter key using a new mock server.
  • PAYGATE_AUDIENCE is now a required environment variable for the gateway, normalized to a canonical HTTP/HTTPS origin at startup.
  • Risk: legacy v1 payment contexts are rejected everywhere by default; any client not upgraded to send v2 binding fields and X-402-Payer will receive 400/402 errors.

Macroscope summarized 58190f9.

Summary by CodeRabbit

  • New Features

    • Introduced request-bound authorization v2, binding signed payments to the gateway, request method, path, content type, and exact request body.
    • Added required payer identification for signed retries.
    • Added clearer authorization errors and enhanced receipt authorization details.
    • Added deterministic end-to-end testing without requiring an external AI provider.
  • Bug Fixes

    • Prevented legacy authorization downgrades and detected tampered requests or signer mismatches.
    • Improved validation of gateway audience configuration and recovered payer identity.
  • Documentation

    • Updated deployment, protocol, API, SDK, and testing documentation for authorization v2 and its configuration.

Co-authored-by: codex <codex@users.noreply.github.com>
@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
microai-paygate Ready Ready Preview, Comment Jul 17, 2026 6:22am

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR makes authorization v2 request-bound by adding audience, method, resource, content type, body hash, and payer validation. It enforces minimum authorization version 2 by default, propagates binding data into receipts, updates SDK/web validation, and adds deterministic E2E and deployment coverage.

Changes

Request-bound authorization

Layer / File(s) Summary
Binding and configuration
gateway/main.go, gateway/request_binding.go, gateway/config.go, gateway/config_test.go
Adds v2 payment-context fields, canonical PAYGATE_AUDIENCE validation, and request binding derived from the exact HTTP request and body.
Gateway verification
gateway/payment_flow.go, gateway/cache.go, gateway/errors.go, gateway/metrics.go
Requires X-402-Payer, reconstructs verifier context, validates recovered signer addresses, maps authorization failures, and records outcomes.
Verifier enforcement
verifier/src/main.rs
Adds MIN_AUTHORIZATION_VERSION, defaults it to 2, and rejects older authorization versions.
Receipts and contracts
gateway/receipt.go, gateway/openapi.yaml, sdk/typescript/src/receipts.ts, web/src/lib/verify-receipt.ts
Carries request-bound authorization metadata through receipts and public schemas.
SDK and web validation
sdk/typescript/src/protocol/*, web/src/lib/x402-client.ts
Rejects legacy or incomplete payment contexts and requires valid v2 request bindings.
E2E and smoke validation
run_e2e.sh, tests/e2e.test.ts, tests/mock-openrouter.ts, web/scripts/deployed-smoke-test.ts
Uses v2 typed-data signing, deterministic provider execution, readiness polling, payer headers, tampering checks, and CORS validation.
Deployment and protocol documentation
.env*, docker-compose.yml, deploy/fly/*, DEPLOY.md, README.md, gateway/README.md, verifier/README.md, web/src/app/docs/protocol/page.mdx
Documents and configures the new audience and minimum authorization-version settings and v2 retry behavior.

Estimated code review effort: 5 (Critical) | ~90 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Gateway
  participant Verifier
  participant ReceiptStore
  Client->>Gateway: request without payment headers
  Gateway-->>Client: 402 challenge with v2 payment context
  Client->>Client: sign PaymentAuthorization v2
  Client->>Gateway: retry with signature and X-402-Payer
  Gateway->>Gateway: reconstruct request binding
  Gateway->>Verifier: verify payer, signature, and authorization version
  Verifier-->>Gateway: valid or mapped rejection
  Gateway->>ReceiptStore: generate receipt with binding metadata
  Gateway-->>Client: response and receipt
Loading

Possibly related PRs

Suggested labels: SWoC26, level:advanced

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.51% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: enforcing request-bound payment authorization v2.
Description check ✅ Passed The description covers summary, security properties, validation commands, and risk; only some template sections like type and affected areas are omitted.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/gateway-payment-authorization-v2

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added documentation Improvements or additions to documentation go Pull requests that update go code github_actions Pull requests that update GitHub Actions code rust Pull requests that update rust code TypeScript TypeScript code type:docs Documentation, API docs, examples, or contributor docs. type:testing Tests, coverage, fixtures, or validation-only work. type:devops Deployment, observability, operations, CI, or infrastructure work. labels Jul 16, 2026
@AnkanMisra AnkanMisra added type:feature New user-facing or API-facing capability. type:security Security, abuse resistance, secret safety, or payment integrity work. labels Jul 16, 2026
Co-authored-by: codex <codex@users.noreply.github.com>
Comment thread gateway/config.go
Comment thread .github/workflows/e2e.yml
Co-authored-by: codex <codex@users.noreply.github.com>
Comment thread run_e2e.sh
Co-authored-by: codex <codex@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4b5abb0d44

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/e2e.yml Outdated
Comment thread gateway/config.go Outdated
Co-authored-by: codex <codex@users.noreply.github.com>
@AnkanMisra

Copy link
Copy Markdown
Owner Author

@codex review Please perform a deep review of the latest commit. Inspect every changed file and trace the payment authorization flow across gateway, verifier, web, SDK, E2E tests, receipts, cache behavior, configuration, deployment files, and documentation. Focus on correctness, security, replay resistance, request binding, cross-service contract parity, failure paths, compatibility, CI coverage, and missing regression tests.

@chatgpt-codex-connector

Copy link
Copy Markdown
Contributor

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: 65d3b33858

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@AnkanMisra

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

if (res.status === 502) {
const text = await res.text();
if (text.includes("upstream_unavailable")) {
expect(true).toBe(true);
return;

P2 Badge Require deterministic E2E to hit the mock provider

In the new deterministic CI path where run_e2e.sh starts the OpenRouter mock when no key is set, the signed happy path should require HTTP 200. Keeping this upstream_unavailable escape means CI still passes if the mock never starts or OPENROUTER_URL wiring breaks, so the new E2E no longer proves the loopback provider path works; remove this fallback or gate it behind an explicit live-provider mode.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread gateway/config.go
Comment thread gateway/payment_flow.go
Comment thread run_e2e.sh
Co-authored-by: codex <codex@users.noreply.github.com>
@AnkanMisra

Copy link
Copy Markdown
Owner Author

Addressed the remaining deterministic E2E finding in 2a6b409. The signed happy path no longer accepts upstream_unavailable and must return HTTP 200 with a result, so CI now proves the loopback mock starts and is wired correctly. The full deterministic E2E suite passes all four cases.

@AnkanMisra

Copy link
Copy Markdown
Owner Author

@codex review Please perform another deep review of the latest commit. Verify the request-bound payment flow, audience validation, deterministic E2E guarantees, deployed smoke test, cross-service contract parity, failure paths, and regression coverage.

Comment thread gateway/config.go Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2a6b4096f2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread web/scripts/deployed-smoke-test.ts
Comment thread run_e2e.sh
Comment thread web/scripts/deployed-smoke-test.ts
Co-authored-by: codex <codex@users.noreply.github.com>
@AnkanMisra

Copy link
Copy Markdown
Owner Author

@codex review Please perform another deep review of the latest commit. Verify IDNA audience canonicalization, port validation, pre-sign smoke-test binding checks, CORS validation, deterministic provider pinning, request-bound payment behavior, and regression coverage.

Comment thread run_e2e.sh
Co-authored-by: codex <codex@users.noreply.github.com>
@AnkanMisra

Copy link
Copy Markdown
Owner Author

@codex review Please perform another deep review of the latest commit. Verify the OpenRouter mock PID and readiness handling, deterministic E2E behavior, audience canonicalization, deployed smoke-test request binding and CORS validation, and the request-bound payment flow end to end.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 21363a4b7f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread web/scripts/deployed-smoke-test.ts
Co-authored-by: codex <codex@users.noreply.github.com>
@AnkanMisra

Copy link
Copy Markdown
Owner Author

@codex review Please perform another deep review of the latest commit. Verify the deployed signed-retry preflight validation, OpenRouter mock readiness handling, deterministic E2E behavior, audience canonicalization, request binding, CORS behavior, and the payment authorization flow end to end.


const checks = [
["signed retry preflight succeeds", response.ok, `HTTP ${response.status}`],
["preflight allows deployed web origin", allowOrigin === ORIGIN, allowOrigin ?? "missing"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium scripts/deployed-smoke-test.ts:174

validateSignedRetryPreflight fails the preflight check when the gateway returns Access-Control-Allow-Origin: *, even though * is a valid CORS response for non-credentialed requests and matches the repo's documented ALLOWED_ORIGINS=* configuration. The test asserts allowOrigin === ORIGIN and exits early on the failure, skipping the rest of the smoke test. The same exact-match comparison on line 204 also rejects *. Accept * in addition to the exact origin in both checks.

-    ["preflight allows deployed web origin", allowOrigin === ORIGIN, allowOrigin ?? "missing"],
+    ["preflight allows deployed web origin", allowOrigin === ORIGIN || allowOrigin === "*", allowOrigin ?? "missing"],
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @web/scripts/deployed-smoke-test.ts around line 174:

`validateSignedRetryPreflight` fails the preflight check when the gateway returns `Access-Control-Allow-Origin: *`, even though `*` is a valid CORS response for non-credentialed requests and matches the repo's documented `ALLOWED_ORIGINS=*` configuration. The test asserts `allowOrigin === ORIGIN` and exits early on the failure, skipping the rest of the smoke test. The same exact-match comparison on line 204 also rejects `*`. Accept `*` in addition to the exact origin in both checks.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
gateway/cache_integration_test.go (1)

106-121: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Assert request-body binding on cache hits.

The mock only checks Signature, so this test would still pass if Line 125 in gateway/cache.go stopped forwarding the body. Capture the verifier request and assert its RequestHash, ideally using two raw JSON bodies with the same cache-key text but different bytes.

As per coding guidelines, “Gateway handlers, config parsing, middleware, timeout behavior, receipts, cache/rate limits, and error paths must have table tests or targeted regression tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gateway/cache_integration_test.go` around lines 106 - 121, The cache
integration test’s mock verifier does not validate that request bodies are
forwarded on cache hits. Update the test around the makeRequest helper and
verifier mock to capture the verifier request and assert its RequestHash,
exercising two raw JSON bodies with identical cache-key text but different bytes
while preserving the existing cache-hit assertions.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/e2e.yml:
- Around line 49-50: Update the Checkout step using actions/checkout to set
persist-credentials to false, ensuring later dependency and E2E steps cannot
access the checkout token through Git configuration.

In `@DEPLOY.md`:
- Line 118: Update the deployment instructions associated with PAYGATE_AUDIENCE
to replace the outdated count of four placeholders with “all placeholders” or an
accurate explicit enumeration, reflecting the six placeholders currently
present.

In `@gateway/config.go`:
- Around line 86-109: Update the audience hostname normalization around
parsed.Hostname() to reject IPv6 zone identifiers, including values decoded from
%25, before rebuilding canonicalHost. Return the existing invalid-hostname error
for zoned addresses and add a regression test covering an escaped IPv6 zone ID
in PAYGATE_AUDIENCE.

In `@gateway/errors_test.go`:
- Around line 219-230: The signer-mismatch regression test currently uses an
HTTP 200 verifier response and does not exercise the non-2xx path. Update
TestHandleSummarizeMapsSignerMismatch to stub a non-2xx verifier status, or add
a separate targeted case, while preserving assertions for the forbidden status,
sanitized response, and signer_mismatch error code.

In `@gateway/README.md`:
- Line 97: Move the PAYGATE_AUDIENCE entry from the “Common optional variables”
section into the README’s required configuration section, preserving its
required status and validation description.

In `@run_e2e.sh`:
- Line 58: Replace the `seq`-based iteration in the readiness loop of
`run_e2e.sh` with Bash arithmetic iteration, preserving the existing 60-attempt
limit and loop body behavior.

In `@tests/README.md`:
- Line 21: Update the prerequisites documentation in tests/README.md to include
port 3100 for the deterministic mock used when OPENROUTER_API_KEY is unset,
while preserving the existing 3000 and 3002 entries and ensuring the documented
ports match the implementation.

In `@web/scripts/deployed-smoke-test.ts`:
- Around line 84-90: Update getChallenge to validate the actual 402 response’s
CORS headers before parsing paymentContext. Require Access-Control-Allow-Origin
to match ORIGIN, while preserving the existing timedFetch and JSON parsing
behavior.

In `@web/src/app/docs/protocol/page.mdx`:
- Line 64: Update the requestHash description in the protocol documentation to
state that it is the SHA-256 hash of the exact serialized request body bytes
only, excluding HTTP headers and other request components.

---

Outside diff comments:
In `@gateway/cache_integration_test.go`:
- Around line 106-121: The cache integration test’s mock verifier does not
validate that request bodies are forwarded on cache hits. Update the test around
the makeRequest helper and verifier mock to capture the verifier request and
assert its RequestHash, exercising two raw JSON bodies with identical cache-key
text but different bytes while preserving the existing cache-hit assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b93ab8b0-e7aa-4eca-b3b5-0ee916d8723d

📥 Commits

Reviewing files that changed from the base of the PR and between 76ed9ff and 58190f9.

📒 Files selected for processing (44)
  • .env.example
  • .env.production.example
  • .github/workflows/e2e.yml
  • DEPLOY.md
  • README.md
  • deploy/fly/gateway.fly.toml
  • deploy/fly/verifier.fly.toml
  • docker-compose.yml
  • gateway/README.md
  • gateway/cache.go
  • gateway/cache_integration_test.go
  • gateway/config.go
  • gateway/config_test.go
  • gateway/errors.go
  • gateway/errors_test.go
  • gateway/go.mod
  • gateway/main.go
  • gateway/metrics.go
  • gateway/metrics_test.go
  • gateway/openapi.yaml
  • gateway/payment_flow.go
  • gateway/payment_flow_test.go
  • gateway/receipt.go
  • gateway/receipt_store_integration_test.go
  • gateway/receipt_test.go
  • gateway/request_binding.go
  • gateway/timeout_test.go
  • run_e2e.sh
  • sdk/typescript/README.md
  • sdk/typescript/src/__tests__/client-flow.test.ts
  • sdk/typescript/src/__tests__/receipts.test.ts
  • sdk/typescript/src/protocol/microai.ts
  • sdk/typescript/src/protocol/types.ts
  • sdk/typescript/src/receipts.ts
  • tests/README.md
  • tests/e2e.test.ts
  • tests/mock-openrouter.ts
  • verifier/README.md
  • verifier/src/main.rs
  • web/scripts/deployed-smoke-test.ts
  • web/src/app/docs/protocol/page.mdx
  • web/src/lib/verify-receipt.ts
  • web/src/lib/x402-client.test.ts
  • web/src/lib/x402-client.ts
💤 Files with no reviewable changes (1)
  • web/src/lib/x402-client.ts

Comment thread .github/workflows/e2e.yml
Comment on lines 49 to 50
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Disable persisted checkout credentials.

Later dependency and E2E steps execute repository-controlled code, which can read the token stored in Git configuration. Set persist-credentials: false.

Proposed fix
       - name: Checkout
         uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+        with:
+          persist-credentials: false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
🧰 Tools
🪛 zizmor (1.26.1)

[warning] 49-50: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/e2e.yml around lines 49 - 50, Update the Checkout step
using actions/checkout to set persist-credentials to false, ensuring later
dependency and E2E steps cannot access the checkout token through Git
configuration.

Source: Linters/SAST tools

Comment thread DEPLOY.md
REDIS_URL=<your-upstash-rediss-url>
RECEIPT_STORE=redis
VERIFIER_URL=https://<verifier-app>.onrender.com
PAYGATE_AUDIENCE=https://<gateway-app>.onrender.com

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the placeholder count in the deployment instructions.

Adding PAYGATE_AUDIENCE introduces another <...> value, but the instructions still say there are four placeholders; this block currently contains six. Change the text to “all placeholders” or enumerate them explicitly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@DEPLOY.md` at line 118, Update the deployment instructions associated with
PAYGATE_AUDIENCE to replace the outdated count of four placeholders with “all
placeholders” or an accurate explicit enumeration, reflecting the six
placeholders currently present.

Source: Coding guidelines

Comment thread gateway/config.go
Comment on lines +86 to +109
hostname := parsed.Hostname()
if !strings.Contains(hostname, ":") {
hostname, err = idna.Lookup.ToASCII(hostname)
if err != nil {
return fmt.Errorf("PAYGATE_AUDIENCE must contain a valid hostname: %w", err)
}
}
hostname = strings.ToLower(hostname)
port := parsed.Port()
if port != "" {
portNumber, err := strconv.Atoi(port)
if err != nil || portNumber < 1 || portNumber > 65535 {
return fmt.Errorf("PAYGATE_AUDIENCE must contain a valid port")
}
}
if (parsed.Scheme == "https" && port == "443") || (parsed.Scheme == "http" && port == "80") {
port = ""
}
canonicalHost := hostname
if port != "" {
canonicalHost = net.JoinHostPort(hostname, port)
} else if strings.Contains(hostname, ":") {
canonicalHost = "[" + hostname + "]"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== gateway/config.go (relevant slice) =="
sed -n '1,180p' gateway/config.go | nl -ba | sed -n '70,130p'

echo
echo "== nearby tests =="
rg -n "PAYGATE_AUDIENCE|zone identifier|Hostname\(" gateway -g '*_test.go' -g '*.go' || true

echo
echo "== Go parser probe for IPv6 zone identifiers =="
cat >/tmp/audience_probe.go <<'EOF'
package main

import (
	"fmt"
	"net/url"
)

func main() {
	for _, raw := range []string{
		"https://[fe80::1%25eth0]",
		"https://[fe80::1%25eth0]:443",
		"https://[fe80::1]",
		"https://example.com",
	} {
		u, err := url.Parse(raw)
		if err != nil {
			fmt.Printf("PARSE %q => err=%v\n", raw, err)
			continue
		}
		fmt.Printf("RAW=%q\n", raw)
		fmt.Printf("  Host=%q Hostname=%q Port=%q Scheme=%q String=%q\n", u.Host, u.Hostname(), u.Port(), u.Scheme, u.String())
	}
}
EOF

go run /tmp/audience_probe.go

Repository: AnkanMisra/MicroAI-Paygate

Length of output: 245


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== gateway/config.go around the audience canonicalization =="
sed -n '1,220p' gateway/config.go | nl -ba | sed -n '1,160p'

echo
echo "== audience-related tests in gateway =="
rg -n "PAYGATE_AUDIENCE|audience" gateway -g '*_test.go' -g '*.go' || true

Repository: AnkanMisra/MicroAI-Paygate

Length of output: 265


🌐 Web query:

Go net/url Hostname IPv6 zone identifier percent-encoding parsed.Hostname returns decoded zone

💡 Result:

In the Go standard library's net/url package, the Hostname method returns the host portion of a URL, stripping away any port number and removing the square brackets typically used for IPv6 literal addresses [1][2]. Regarding IPv6 zone identifiers, the implementation adheres to RFC 6874, which specifies that the zone identifier is introduced by a percent-encoded percent sign ("%25") [3][2][4]. When a URL containing an IPv6 address with a zone identifier is parsed, the net/url package handles the zone identifier by unescaping it [3][2]. Specifically, the internal logic splits the host into a host part (before the "%25") and a zone part (starting from the "%25") [3][2]. The host part is unescaped according to standard host rules, while the zone part is unescaped using rules specific to zone identifiers (which allow a wider range of percent-encoded characters) [3][2]. The Hostname method effectively returns the resulting unescaped host string [3][2]. Recent updates to the Go repository (as of April 2026) have introduced stricter validation for these zone identifiers to ensure compliance with the RFC 6874 ABNF grammar, limiting characters within the zone identifier to unreserved characters (ALPHA, DIGIT, '-', '.', '_', '~') or valid percent-encoded sequences [5][6]. This prevents improper greedy matching of characters—such as closing brackets—that should not appear unescaped within the zone identifier [5][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== gateway/config.go audience logic =="
sed -n '80,125p' gateway/config.go

echo
echo "== gateway tests mentioning PAYGATE_AUDIENCE or URL host parsing =="
rg -n "PAYGATE_AUDIENCE|Hostname\(|zone identifier|fe80::1" gateway -g '*_test.go' -g '*.go' || true

Repository: AnkanMisra/MicroAI-Paygate

Length of output: 4229


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cat >/tmp/url_zone_probe.go <<'EOF'
package main

import (
	"fmt"
	"net/url"
)

func main() {
	inputs := []string{
		"https://[fe80::1%25eth0]",
		"https://[fe80::1%25eth0]:443",
		"https://[fe80::1]",
	}
	for _, raw := range inputs {
		u, err := url.Parse(raw)
		if err != nil {
			fmt.Printf("PARSE %q => err=%v\n", raw, err)
			continue
		}
		fmt.Printf("RAW=%q\n", raw)
		fmt.Printf("  Host=%q Hostname=%q Port=%q EscapedPath=%q String=%q\n", u.Host, u.Hostname(), u.Port(), u.EscapedPath(), u.String())
	}
}
EOF

go run /tmp/url_zone_probe.go

Repository: AnkanMisra/MicroAI-Paygate

Length of output: 563


Reject IPv6 zone IDs in PAYGATE_AUDIENCE

gateway/config.go:86-109parsed.Hostname() decodes %25 to %, so rebuilding with [ + hostname + ] turns https://[fe80::1%25eth0] into https://[fe80::1%eth0]. Reject zone identifiers here, or preserve the escape before normalizing, and add a regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gateway/config.go` around lines 86 - 109, Update the audience hostname
normalization around parsed.Hostname() to reject IPv6 zone identifiers,
including values decoded from %25, before rebuilding canonicalHost. Return the
existing invalid-hostname error for zoned addresses and add a regression test
covering an escaped IPv6 zone ID in PAYGATE_AUDIENCE.

Source: Coding guidelines

Comment thread gateway/errors_test.go
Comment on lines +219 to +230
func TestHandleSummarizeMapsSignerMismatch(t *testing.T) {
withVerifierResponse(t, http.StatusOK, `{"is_valid":false,"recovered_address":"0x0000000000000000000000000000000000000001","error":"SENSITIVE_SIGNER_DETAIL","error_code":"signer_mismatch"}`)

router := newSummarizeTestRouter()
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, signedSummarizeRequest(`{"text":"hello"}`))

require.Equal(t, http.StatusForbidden, recorder.Code)
require.NotContains(t, recorder.Body.String(), "SENSITIVE_SIGNER_DETAIL")
var response map[string]string
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &response))
require.Equal(t, "signer_mismatch", response["error"])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Exercise the non-2xx signer-mismatch path.

The HTTP 200 stub tests public mapping but never covers the newly added isVerifierBusinessRejection branch. Return a non-2xx verifier status in this case, or add a second case for it.

Proposed regression coverage
-	withVerifierResponse(t, http.StatusOK, `{"is_valid":false,"recovered_address":"0x0000000000000000000000000000000000000001","error":"SENSITIVE_SIGNER_DETAIL","error_code":"signer_mismatch"}`)
+	withVerifierResponse(t, http.StatusForbidden, `{"is_valid":false,"recovered_address":"0x0000000000000000000000000000000000000001","error":"SENSITIVE_SIGNER_DETAIL","error_code":"signer_mismatch"}`)

As per coding guidelines, “Gateway handlers, config parsing, middleware, timeout behavior, receipts, cache/rate limits, and error paths must have table tests or targeted regression tests.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestHandleSummarizeMapsSignerMismatch(t *testing.T) {
withVerifierResponse(t, http.StatusOK, `{"is_valid":false,"recovered_address":"0x0000000000000000000000000000000000000001","error":"SENSITIVE_SIGNER_DETAIL","error_code":"signer_mismatch"}`)
router := newSummarizeTestRouter()
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, signedSummarizeRequest(`{"text":"hello"}`))
require.Equal(t, http.StatusForbidden, recorder.Code)
require.NotContains(t, recorder.Body.String(), "SENSITIVE_SIGNER_DETAIL")
var response map[string]string
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &response))
require.Equal(t, "signer_mismatch", response["error"])
func TestHandleSummarizeMapsSignerMismatch(t *testing.T) {
withVerifierResponse(t, http.StatusForbidden, `{"is_valid":false,"recovered_address":"0x0000000000000000000000000000000000000001","error":"SENSITIVE_SIGNER_DETAIL","error_code":"signer_mismatch"}`)
router := newSummarizeTestRouter()
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, signedSummarizeRequest(`{"text":"hello"}`))
require.Equal(t, http.StatusForbidden, recorder.Code)
require.NotContains(t, recorder.Body.String(), "SENSITIVE_SIGNER_DETAIL")
var response map[string]string
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &response))
require.Equal(t, "signer_mismatch", response["error"])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gateway/errors_test.go` around lines 219 - 230, The signer-mismatch
regression test currently uses an HTTP 200 verifier response and does not
exercise the non-2xx path. Update TestHandleSummarizeMapsSignerMismatch to stub
a non-2xx verifier status, or add a separate targeted case, while preserving
assertions for the forbidden status, sanitized response, and signer_mismatch
error code.

Source: Coding guidelines

Comment thread gateway/README.md
| `OLLAMA_MODEL` | `llama2` provider default | Used when `AI_PROVIDER=ollama`. |
| `VERIFIER_URL` | **required** (no fallback) | Where the gateway calls `/verify`. Use `http://127.0.0.1:3002` for `bun run stack`, `http://verifier:3002` in Compose, the platform's HTTPS URL in production. The gateway refuses to start if unset. |
| `PAYGATE_AUDIENCE` | unset | Public gateway origin reserved for request-bound authorization v2. Configure the production origin before the gateway cutover; the current v1 gateway does not read it. |
| `PAYGATE_AUDIENCE` | **required** | Trusted public gateway origin included in every v2 authorization. Origins only; paths, queries, fragments, credentials, and non-HTTP schemes are rejected at startup. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Move PAYGATE_AUDIENCE out of the optional configuration section.

This variable is documented as required, but it remains under “Common optional variables.” Move it into a required configuration section or rename the heading so operators do not omit a startup-required setting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gateway/README.md` at line 97, Move the PAYGATE_AUDIENCE entry from the
“Common optional variables” section into the README’s required configuration
section, preserving its required status and validation description.

Source: Coding guidelines

Comment thread run_e2e.sh
# Wait for both services to be ready instead of assuming a fixed build/start time.
echo "Waiting for services to initialize..."
SERVICES_READY=false
for _ in $(seq 1 60); do

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Avoid seq in the macOS-compatible runner.

seq is not available on stock macOS, so the readiness loop fails immediately. Use Bash arithmetic instead.

Proposed fix
-for _ in $(seq 1 60); do
+for ((attempt = 1; attempt <= 60; attempt++)); do
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for _ in $(seq 1 60); do
for ((attempt = 1; attempt <= 60; attempt++)); do
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@run_e2e.sh` at line 58, Replace the `seq`-based iteration in the readiness
loop of `run_e2e.sh` with Bash arithmetic iteration, preserving the existing
60-attempt limit and loop body behavior.

Comment thread tests/README.md
- Rust toolchain
- Ports `3000` and `3002` free
- `OPENROUTER_API_KEY` for the default OpenRouter gateway startup path
- No external AI key is required; the helper starts a deterministic OpenRouter-compatible mock when `OPENROUTER_API_KEY` is unset.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the mock’s required port.

The helper binds the deterministic mock to port 3100, but the prerequisites list only 3000 and 3002. Add port 3100 when OPENROUTER_API_KEY is unset.

As per coding guidelines, Markdown changes must be checked for wrong ports and mismatch with code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/README.md` at line 21, Update the prerequisites documentation in
tests/README.md to include port 3100 for the deterministic mock used when
OPENROUTER_API_KEY is unset, while preserving the existing 3000 and 3002 entries
and ensuring the documented ports match the implementation.

Source: Coding guidelines

Comment on lines +84 to 90
async function getChallenge(body: string): Promise<PaymentContext> {
const r = await timedFetch(`${GATEWAY}/api/ai/summarize`, {
method: "POST",
headers: { "Content-Type": "application/json", Origin: ORIGIN },
body: JSON.stringify({ text }),
body,
});
return (await r.json()).paymentContext as PaymentContext;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate CORS on the actual 402 challenge response.

Bun does not enforce browser CORS. The script currently checks preflight and the signed response, but a 402 without Access-Control-Allow-Origin would still pass here while browsers could not read paymentContext.

Proposed validation
   const r = await timedFetch(`${GATEWAY}/api/ai/summarize`, {
     method: "POST",
     headers: { "Content-Type": "application/json", Origin: ORIGIN },
     body,
   });
-  return (await r.json()).paymentContext as PaymentContext;
+  const allowOrigin = r.headers.get("access-control-allow-origin");
+  rec("challenge returns 402", r.status === 402, `HTTP ${r.status}`);
+  rec("challenge allows deployed web origin", allowOrigin === ORIGIN, allowOrigin ?? "missing");
+  return (await r.json()).paymentContext as PaymentContext;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function getChallenge(body: string): Promise<PaymentContext> {
const r = await timedFetch(`${GATEWAY}/api/ai/summarize`, {
method: "POST",
headers: { "Content-Type": "application/json", Origin: ORIGIN },
body: JSON.stringify({ text }),
body,
});
return (await r.json()).paymentContext as PaymentContext;
async function getChallenge(body: string): Promise<PaymentContext> {
const r = await timedFetch(`${GATEWAY}/api/ai/summarize`, {
method: "POST",
headers: { "Content-Type": "application/json", Origin: ORIGIN },
body,
});
const allowOrigin = r.headers.get("access-control-allow-origin");
rec("challenge returns 402", r.status === 402, `HTTP ${r.status}`);
rec("challenge allows deployed web origin", allowOrigin === ORIGIN, allowOrigin ?? "missing");
return (await r.json()).paymentContext as PaymentContext;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/scripts/deployed-smoke-test.ts` around lines 84 - 90, Update getChallenge
to validate the actual 402 response’s CORS headers before parsing
paymentContext. Require Access-Control-Allow-Origin to match ORIGIN, while
preserving the existing timedFetch and JSON parsing behavior.

- `method`: uppercase HTTP method.
- `resource`: encoded path plus raw query string.
- `contentType`: exact request content type.
- `requestHash`: SHA-256 hash of the exact serialized request bytes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Specify that requestHash covers only the serialized body bytes.

Line 64 says “request bytes,” which can be interpreted as the entire HTTP request. The gateway and SDK hash only the exact serialized request body, so independent clients following this text could generate incompatible signatures.

-- `requestHash`: SHA-256 hash of the exact serialized request bytes.
+- `requestHash`: SHA-256 hash of the exact serialized request body bytes.

As per coding guidelines, Markdown must be checked for “mismatch with code/workflows.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- `requestHash`: SHA-256 hash of the exact serialized request bytes.
- `requestHash`: SHA-256 hash of the exact serialized request body bytes.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/app/docs/protocol/page.mdx` at line 64, Update the requestHash
description in the protocol documentation to state that it is the SHA-256 hash
of the exact serialized request body bytes only, excluding HTTP headers and
other request components.

Source: Coding guidelines

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 58190f93b5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread DEPLOY.md
CHAIN_ID=84532
EXPECTED_CHAIN_ID=84532
PAYMENT_AMOUNT=0.001
ALLOWED_ORIGINS=*

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use a concrete origin for deployed CORS

In this Render gateway env block, ALLOWED_ORIGINS=* does not actually allow the deployed web app: getAllowedOrigins only accepts absolute http(s) origins, so * is logged as invalid and the gateway falls back to http://localhost:3001. When following these deploy docs, the new signed-retry preflight from Vercel will miss Access-Control-Allow-Origin and the browser cannot send the X-402-* headers; please document the exact web origin here or add intentional wildcard support.

Useful? React with 👍 / 👎.

Comment thread tests/mock-openrouter.ts
Comment on lines +10 to +11
if (request.method !== "POST") {
return new Response("Not found", { status: 404 });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serve the OpenRouter models path from the mock

When run_e2e.sh points OPENROUTER_URL at the deterministic mock, the gateway readiness check derives /api/v1/models from that URL, but this mock only returns 200 for GET /health and returns 404 for every other GET. That means /readyz reports OpenRouter as degraded under deterministic runs even though the wait loop says the mock is ready; please also handle the models endpoint or align the health check with the mock.

Useful? React with 👍 / 👎.

Comment thread gateway/README.md
C->>G: POST /api/ai/summarize
G-->>C: 402 + paymentContext(recipient, token, amount, chainId, nonce, timestamp)
C->>G: Retry with X-402-Signature, X-402-Nonce, X-402-Timestamp
C->>G: Retry with X-402-Signature, nonce, timestamp, payer

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Use full retry header names in the flow diagram

This updated sequence now documents the retry as bare nonce, timestamp, payer instead of the actual X-402-Nonce, X-402-Timestamp, and new X-402-Payer headers. Since the v2 cutover depends on the browser preflight and gateway seeing those exact header names, copying this diagram can produce a signed retry that is rejected; please keep the full header names here.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation github_actions Pull requests that update GitHub Actions code go Pull requests that update go code rust Pull requests that update rust code type:devops Deployment, observability, operations, CI, or infrastructure work. type:docs Documentation, API docs, examples, or contributor docs. type:feature New user-facing or API-facing capability. type:security Security, abuse resistance, secret safety, or payment integrity work. type:testing Tests, coverage, fixtures, or validation-only work. TypeScript TypeScript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant