Skip to content

Expose Bazaar schemas from OpenAPI payment challenges - #429

Open
epistemedeus wants to merge 1 commit into
solana-foundation:mainfrom
epistemedeus:fix/openapi-bazaar-schema
Open

Expose Bazaar schemas from OpenAPI payment challenges#429
epistemedeus wants to merge 1 commit into
solana-foundation:mainfrom
epistemedeus:fix/openapi-bazaar-schema

Conversation

@epistemedeus

Copy link
Copy Markdown

Summary

  • project the exact matched OpenAPI operation into an x402 v2 Bazaar extension
  • materialize local schema references so the payment envelope is self-contained
  • preserve existing extension keys and leave all payment requirements unchanged
  • omit the extension when a complete invocation or successful JSON response contract cannot be derived

Verification

  • cargo test -p pay-core --features server bazaar_projection --lib
  • cargo test -p pay-core --features server x402_extension_attachment_preserves_existing_keys --lib
  • cargo check -p pay
  • cargo clippy -p pay-core --features server --lib -- -D warnings

Fixes #428

@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

@epistemedeus is attempting to deploy a commit to the Solana Foundation Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps

greptile-apps Bot commented Aug 11, 2026

Copy link
Copy Markdown

Greptile Summary

This change adds Bazaar discovery metadata to payment challenges by projecting the matched OpenAPI operation's parameters, JSON request body, and successful JSON response schema.

Focused checks confirmed that literal routes take precedence over matching template routes, concrete requests retain metadata for template routes, operation-level parameter overrides replace inherited requiredness, and optional JSON request bodies remain optional.

The pull request does not meet the repository's verified-commit requirement because the current head commit is unsigned.

Confidence Score: 4/5

Do not merge until the head commit is replaced with a verified signed commit.

The remaining blocking issue is the repository rule requiring a verified signature on the current head commit.

Files Needing Attention: rust/crates/core/src/lib.rs

T-Rex T-Rex Logs

What T-Rex did

  • The route matching logic in rust/crates/core/src/server/openapi.rs was analyzed and shown to allow both exact and templated paths, with the runtime selecting the literal path '/projects/123' before the templated one due to the key order in serde_json::Map.
  • A focused Bazaar projects route test was executed; the prior revision had no Bazaar resolver, and the latest test for the concrete projects route passed (1/1).
  • The optional operation parameter override was validated by a focused test, and the resulting projection shows limit as a string with no required member.
  • The Bazaar optional body tests were exercised, and the focused test for POST/PUT/PATCH JSON bodies passed (1 passed, 0 failed).

View all artifacts

T-Rex Ran code and verified through T-Rex

Reviews (2): Last reviewed commit: "Expose Bazaar schemas from OpenAPI chall..." | Re-trigger Greptile

Comment on lines +1228 to +1235
let (path_item, operation) = paths.iter().find_map(|(path, item)| {
let combined = if base_path.is_empty() {
normalize_path(path)
} else {
format!("{}/{}", base_path, path.trim_start_matches('/'))
};
if canonical_path(&combined) != target {
return None;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Template routes lose Bazaar metadata

When a paid OpenAPI route contains a path parameter, this compares a concrete request path such as projects/123 with the canonicalized template projects/{*} using string equality. The operation is therefore not found and the PAYMENT-REQUIRED challenge omits Bazaar discovery metadata for the route.

Artifacts

Focused Rust regression-test source

  • Captures the executed test that calls the Bazaar operation matcher with OpenAPI path `/projects/{id}` and concrete request path `/projects/123`, with the takeaway that concrete paths are required to resolve templated operations.

Observed templated-path test failure

  • Shows the focused Rust test ran one test and failed at the assertion that concrete `/projects/123` must resolve Bazaar metadata from `/projects/{id}`, with the takeaway that the finding is reproduced.

View artifacts

T-Rex Ran code and verified through T-Rex

Comment on lines +1252 to +1316
for parameter in path_item
.get("parameters")
.and_then(Value::as_array)
.into_iter()
.flatten()
.chain(
operation
.get("parameters")
.and_then(Value::as_array)
.into_iter()
.flatten(),
)
{
let parameter = resolve_openapi_object(doc, parameter)?;
let location = parameter.get("in").and_then(Value::as_str)?;
let name = parameter.get("name").and_then(Value::as_str)?.to_string();
let normalized_name = name.to_ascii_lowercase();
if location == "header"
&& matches!(
normalized_name.as_str(),
"authorization" | "payment-signature" | "x-payment" | "payment-required"
)
{
continue;
}
let schema = parameter.get("schema")?;
let schema = materialize_local_refs(doc, schema, &mut HashSet::new(), 0)?;
let required = parameter
.get("required")
.and_then(Value::as_bool)
.unwrap_or(location == "path");
let example = parameter
.get("example")
.or_else(|| parameter.get("schema").and_then(|s| s.get("example")))
.or_else(|| parameter.get("schema").and_then(|s| s.get("default")))
.cloned();
match location {
"query" => {
query_properties.insert(name.clone(), schema);
if let Some(example) = example {
query_examples.insert(name.clone(), example);
}
if required {
query_required.push(Value::String(name));
}
}
"path" => {
path_properties.insert(name.clone(), schema);
if let Some(example) = example {
path_examples.insert(name.clone(), example);
}
if required {
path_required.push(Value::String(name));
}
}
"header" => {
header_properties.insert(name.clone(), schema);
if let Some(example) = example {
header_examples.insert(name.clone(), example);
}
if required {
header_required.push(Value::String(name));
}
}
_ => {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Parameter overrides retain stale requiredness

When an operation overrides a path-level parameter with the same name and location, the property schema is replaced but the old required name remains appended. The generated Bazaar contract can therefore combine the operation-level schema with path-level requiredness and reject a valid invocation.

T-Rex Ran code and verified through T-Rex

Comment thread rust/crates/core/src/server/openapi.rs Outdated
Comment on lines +1356 to +1372
if body_method {
let request_body = operation.get("requestBody")?;
let request_body = resolve_openapi_object(doc, request_body)?;
let media = json_media_type(request_body.get("content")?.as_object()?)?;
let body_schema =
materialize_local_refs(doc, media.get("schema")?, &mut HashSet::new(), 0)?;
input_info.insert("bodyType".to_string(), json!("json"));
input_info.insert(
"body".to_string(),
media.get("example").cloned().unwrap_or_else(|| json!({})),
);
input_schema_properties.insert(
"bodyType".to_string(),
json!({ "type": "string", "enum": ["json"] }),
);
input_schema_properties.insert("body".to_string(), body_schema);
input_required.extend([json!("bodyType"), json!("body")]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Optional request bodies become mandatory

For POST, PUT, and PATCH operations with an OpenAPI JSON requestBody marked required: false, this always adds bodyType and body to the required input fields. Bazaar clients are consequently told that bodyless invocations are invalid even though the operation accepts them.

Artifacts

Optional request body reproduction source

  • The executable Rust test constructs an OpenAPI POST requestBody with required:false and asserts that Bazaar does not require body fields; it is the focused reproduction source.

Optional request body reproduction output

  • Cargo executed the focused test and printed the Bazaar input schema requiring bodyType and body before the expected assertion failure; the finding is reproduced.

View artifacts

T-Rex Ran code and verified through T-Rex

Comment on lines +50 to +55
/// Filtered OpenAPI document served by this gateway, when available.
/// The payment gate uses it only to derive optional machine-discovery
/// metadata for the exact method and route being challenged.
fn openapi_document(&self) -> Option<&serde_json::Value> {
None
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Commit lacks verified signature

Commit 098782b9155c7400711e8f9c5f5ae1d3288b311b has no verified cryptographic signature, so this pull request does not satisfy the repository owner’s signed-commit merge requirement.

Context Used: Request changes if the commits are not signed (ver... (source)

@epistemedeus
epistemedeus force-pushed the fix/openapi-bazaar-schema branch from 098782b to d50886b Compare August 11, 2026 19:31
@epistemedeus

Copy link
Copy Markdown
Author

Updated the branch with GitHub-verified signed commit d50886b and addressed all three reproduced Bazaar contract failures: concrete requests now match templated OpenAPI routes, operation-level parameters atomically override path-level schema and requiredness, and optional POST, PUT, or PATCH request bodies remain optional. Five focused Bazaar regressions and all 52 OpenAPI tests pass. cargo clippy -p pay-core --features server -- -D warnings passes. The complete pay-core server-feature suite passes: 778 unit tests, 6 config tests, 55 metering tests, 34 server tests with one existing ignore, 9 surfpool tests, and 3 network tests. The remaining Vercel preview status requires Solana Foundation team authorization.

#[cfg(feature = "server")]
pub trait PaymentState: Clone + Send + Sync + 'static {
fn apis(&self) -> &[ApiSpec];
/// Filtered OpenAPI document served by this gateway, when available.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Replacement commit remains unsigned

The current PR head commit d50886b965c32caf77e1817cde16b8609c42cab4 has no cryptographic signature, so the pull request still violates the repository owner's verified-commit merge requirement.

Context Used: Request changes if the commits are not signed (ver... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose x402 Bazaar input and output schemas from supplied OpenAPI

1 participant