From 530e54dac7289d41d93c57a21a19ecf39e4e5380 Mon Sep 17 00:00:00 2001 From: Jake Fineman Date: Thu, 3 Sep 2026 09:42:22 -0400 Subject: [PATCH] docs(x402): document error_detail.payment_rejected on the 402 challenge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #46: the 402 x402 challenge contract omitted error_detail.payment_rejected, so generated clients had no way to discover the deny reason on a rejected payment. - Extracted the Error envelopes inner object as the ErrorBody component schema and re-pointed X402PaymentRequired.error_detail at it (allOf + payment_rejected) instead of the wrapped Error schema — the gateway nests the bare error object directly under error_detail with no inner "error" key, confirmed against a live 402 receipt from api.wave.online. Every other Error usage is unchanged (Error still requires "error": ErrorBody). - Documents error_detail.payment_rejected: { reason, rail }, present only when a submitted payment credential was rejected. reason is a stable token (generic token payment_rejected, or a more specific one naming the failing condition); rail names the payment rail that rejected it. Verified against a live rejected- payment receipt. - Regenerated generated/api-types.d.ts from the updated spec (sdk-types CI gate). - CHANGELOG.md: Fixed (error_detail nesting) + Added (payment_rejected) entries. Supersedes #45 (stale branch, conflicts with current main; re-applied by hand). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01K9mRh8G2ugbUt2kaXvFvF6 --- CHANGELOG.md | 16 +++++++ generated/api-types.d.ts | 48 ++++++++++++-------- openapi.yaml | 96 ++++++++++++++++++++++++++-------------- 3 files changed, 108 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec73469..3c7c32e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,14 @@ All notable changes to this project are documented here. The format is based on now declined with a warning rather than silently running. (wave-av/wave-foundation-public#73) +- **`X402PaymentRequired.error_detail` nesting** (`openapi.yaml`): `error_detail` referenced the + `Error` envelope (`{ error: { code, ... } }`), but the gateway nests the bare error object + directly under `error_detail` (`{ code, message, ... }`), with no inner `error` wrapper — + confirmed against a live 402 receipt (`curl https://api.wave.online/v1/clips`). The inner + object is now extracted as the `ErrorBody` component schema (referenced by `Error`, so every + other response is unchanged) and `error_detail` composes `ErrorBody` instead, matching the wire + shape so generated types no longer expect a nonexistent `error_detail.error` member. + ### Added - **Fleet agent directory resolve** (`GET /identity/resolve`) — identity-fabric E1. Adds the @@ -65,6 +73,14 @@ All notable changes to this project are documented here. The format is based on tag description explains the direct-to-relay flow, the `join` query parameter / `x-wave-moq-join` header token carriers, and pins the surface to `draft-ietf-moq-transport-18` (draft-19, published 2026-07-06, is not yet deployed). +- **`X402PaymentRequired.error_detail.payment_rejected`** (`openapi.yaml`) — documents the + additive `{ reason, rail }` deny verdict the gateway now publishes on a 402 when a submitted + payment was REJECTED, so generated types and docs can discover the diagnostic field. Optional + and conditional: absent on an ordinary unpaid challenge, present with a rejection reason token + and rail name when a submitted credential was denied — confirmed against a live rejected-payment + receipt (`curl -H "X-PAYMENT: " https://api.wave.online/v1/clips`), which returned + `error_detail.payment_rejected: { reason: "invalid_payment_header", rail: "base-usdc" }`. + Closes #46. - **WAVE Attestation Standard v1** (`attestation/` directory): - `attestation/ATTESTATION-STANDARD-v1.md` — Frozen v1 specification: wire envelope shape, field types, canonicalization algorithm (`canonicalJson` + `attestationId`), diff --git a/generated/api-types.d.ts b/generated/api-types.d.ts index 4916b8d..537e6b9 100644 --- a/generated/api-types.d.ts +++ b/generated/api-types.d.ts @@ -1013,25 +1013,27 @@ export interface components { schemas: { /** @description WAVE-normalized error envelope returned by the api.wave.online gateway. Upstream provider, auth, and quota failures are *interpreted* into this single shape — raw upstream errors (stack traces, vendor SDK objects) are never leaked to the caller. `error.code` is a stable machine-readable code (e.g. AUTH_REQUIRED, SCOPE_OVERREACH, RATE_LIMIT_EXCEEDED, UPSTREAM_ERROR); `error.message` is human-readable. */ Error: { - error: { - /** @description Stable machine-readable error code. */ - code: string; - /** @description Human-readable explanation, safe to surface to end users. */ - message: string; - /** @description Optional structured context (e.g. failing field, retry limit, requested scopes) — never raw upstream payloads. */ - details?: { - [key: string]: unknown; - }; - /** @description Actionable next steps, ordered most→least likely to resolve the error. Written to be acted on by a human OR an agent. */ - suggestions?: string[]; - /** @description Closest valid alternatives when the caller likely made a typo or wrong choice (e.g. an unknown scope, product, or route). */ - did_you_mean?: string[]; - /** - * Format: uri - * @description Documentation link for this error. - */ - doc_url?: string; + error: components["schemas"]["ErrorBody"]; + }; + /** @description The normalized WAVE error object itself — the value of the `Error` envelope's `error` member. Referenced directly by bodies that carry the error object under a different key (the 402 x402 challenge nests it under `error_detail` without the envelope wrapper). */ + ErrorBody: { + /** @description Stable machine-readable error code. */ + code: string; + /** @description Human-readable explanation, safe to surface to end users. */ + message: string; + /** @description Optional structured context (e.g. failing field, retry limit, requested scopes) — never raw upstream payloads. */ + details?: { + [key: string]: unknown; }; + /** @description Actionable next steps, ordered most→least likely to resolve the error. Written to be acted on by a human OR an agent. */ + suggestions?: string[]; + /** @description Closest valid alternatives when the caller likely made a typo or wrong choice (e.g. an unknown scope, product, or route). */ + did_you_mean?: string[]; + /** + * Format: uri + * @description Documentation link for this error. + */ + doc_url?: string; }; Pagination: { page?: number; @@ -1920,7 +1922,15 @@ export interface components { error: string; /** @description Payment options; sign one and retry with the `x-payment` header. */ accepts: components["schemas"]["X402Accepts"][]; - error_detail?: components["schemas"]["Error"]; + error_detail?: components["schemas"]["ErrorBody"] & { + /** @description Present ONLY when a submitted payment was REJECTED (as opposed to no payment having been made at all): the sanitized deny verdict, so a paying caller can tell a rejected payment apart from an unpaid challenge and correct the named field. `reason` is a lowercase snake_case token from the gateway's closed deny vocabulary (e.g. `invalid_permit_header`, `session_expired`, `duplicate`, `rate_limited`); internal detail that cannot be published verbatim collapses to the generic `payment_rejected`. `rail` names the payment rail that denied (e.g. `base-usdc`, `tempo-pathusd`), or `unknown`. */ + payment_rejected?: { + /** @description Sanitized machine-readable rejection reason token. */ + reason: string; + /** @description The payment rail the rejection occurred on. */ + rail: string; + }; + }; /** @description Machine-executable directive for agent callers — a `pay` directive carrying the same `accepts` options. */ next_action?: { [key: string]: unknown; diff --git a/openapi.yaml b/openapi.yaml index e865763..c206051 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2367,39 +2367,47 @@ components: required: [error] properties: error: + $ref: '#/components/schemas/ErrorBody' + + ErrorBody: + type: object + description: > + The normalized WAVE error object itself — the value of the `Error` envelope's + `error` member. Referenced directly by bodies that carry the error object under + a different key (the 402 x402 challenge nests it under `error_detail` without + the envelope wrapper). + required: [code, message] + properties: + code: + type: string + description: Stable machine-readable error code. + message: + type: string + description: Human-readable explanation, safe to surface to end users. + details: type: object - required: [code, message] - properties: - code: - type: string - description: Stable machine-readable error code. - message: - type: string - description: Human-readable explanation, safe to surface to end users. - details: - type: object - additionalProperties: true - description: > - Optional structured context (e.g. failing field, retry limit, - requested scopes) — never raw upstream payloads. - suggestions: - type: array - description: > - Actionable next steps, ordered most→least likely to resolve the - error. Written to be acted on by a human OR an agent. - items: - type: string - did_you_mean: - type: array - description: > - Closest valid alternatives when the caller likely made a typo or - wrong choice (e.g. an unknown scope, product, or route). - items: - type: string - doc_url: - type: string - format: uri - description: Documentation link for this error. + additionalProperties: true + description: > + Optional structured context (e.g. failing field, retry limit, + requested scopes) — never raw upstream payloads. + suggestions: + type: array + description: > + Actionable next steps, ordered most→least likely to resolve the + error. Written to be acted on by a human OR an agent. + items: + type: string + did_you_mean: + type: array + description: > + Closest valid alternatives when the caller likely made a typo or + wrong choice (e.g. an unknown scope, product, or route). + items: + type: string + doc_url: + type: string + format: uri + description: Documentation link for this error. Pagination: type: object @@ -4058,7 +4066,29 @@ components: items: $ref: '#/components/schemas/X402Accepts' error_detail: - $ref: '#/components/schemas/Error' + allOf: + - $ref: '#/components/schemas/ErrorBody' + - type: object + properties: + payment_rejected: + type: object + description: >- + Present ONLY when a submitted payment was REJECTED (as opposed to no payment + having been made at all): the sanitized deny verdict, so a paying caller can + tell a rejected payment apart from an unpaid challenge and correct the named + field. `reason` is a lowercase snake_case token from the gateway's closed deny + vocabulary (e.g. `invalid_permit_header`, `session_expired`, `duplicate`, + `rate_limited`); internal detail that cannot be published verbatim collapses + to the generic `payment_rejected`. `rail` names the payment rail that denied + (e.g. `base-usdc`, `tempo-pathusd`), or `unknown`. + required: [reason, rail] + properties: + reason: + type: string + description: Sanitized machine-readable rejection reason token. + rail: + type: string + description: The payment rail the rejection occurred on. next_action: type: object additionalProperties: true