Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Added

- **D.3 indexer-state probe + `upstream_stuck` composite verdict** ([X402-46](https://vahdatfardin.atlassian.net/browse/X402-46), ADR-004 Pillar 1 + Pillar 3). The existing `indexing` check now emits a `detail.indexer_state` facet with four values: `indexed` (≥1 resource in CDP discovery), `processing` (404 or empty resources — Max's polyodds.bet case + the canonical #2207 indexer-state-stuck cluster), `unknown` (HTTP error / non-JSON / network failure), and `not_applicable_non_cdp` (operator declared a non-CDP facilitator in their well-known manifest; CDP discovery is not the canonical indexer for this service per ADR-004 Pillar 3). When `indexer_state: processing` fires the verdict synthesizer returns a **new composite verdict `upstream_stuck`** — distinct from generic `upstream_issue` because the root cause is known (facilitator settled, indexer queue stalled). `upstream_stuck` rolls up to exit code 3 (preserves CI contract per ADR-004); verdict prose + JSON facets carry the granularity. New shared `src/bazaar/facilitator-detect.ts` helper — manifest-claim-based detection (`extensions.bazaar.facilitator` value), used by both D.2 (propagation) and D.3 (indexing). When facilitator is non-CDP, both D.2 and D.3 short-circuit to `not_applicable_non_cdp` BEFORE the network probe — fast, no HTTP cost, no false-positive. **v0.3.3+ deferred:** `processing_fresh` vs `processing_stale` distinction (requires settle-timestamp data we don't collect without driving live settles; future work via operator-supplied evidence flag).
- **D.2 metadata propagation diff** ([X402-45](https://vahdatfardin.atlassian.net/browse/X402-45)). New 5th check in `bazaar-check` results: `propagation`. Queries CDP discovery for the service's `payTo` and diffs the rendered fields (`name`, `description`) against what the operator declared in `/.well-known/x402`. Surfaces the @zev / TheRoosters / GM pain pattern: manifest is correctly shaped, indexer dropped fields, listing renders blank — your implementation is fine, the gating step is upstream. Emits `detail.metadata_propagation` with one of four states: `ok` (all diffed fields match — status pass), `partial` (some drift — status info + diff array showing per-field mismatches), `missing` (indexer surfaces none of the manifest's declared fields, matching the canonical #2207 indexer-state cluster — status info), `unknown` (defensive default when no manifest is available via `--endpoint` mode, no `payTo` is extractable, or the discovery query failed — status pass). New module `src/bazaar/propagation.ts`; pure-function `computePropagationStatus()` helper exported for downstream re-use. Diff scope intentionally narrow (`name`, `description`) — extends when new pain shapes surface. **Facilitator-aware semantics layered separately:** D.3 (X402-46) will add the explicit `not_applicable_non_cdp` state per ADR-004 Pillar 3; D.2 stays narrow and returns `unknown` for non-CDP services rather than asserting attribution it can't yet determine.
- **`bazaar-check --log json` is now a documented public API contract** ([X402-44](https://vahdatfardin.atlassian.net/browse/X402-44), ADR-004 Pillar 2). Three deliverables landed together: the contract doc at [`src/bazaar/json-api.md`](./src/bazaar/json-api.md) (envelope shape + per-check `detail` keys + verdict discriminator + stability rules + regeneration workflow), a frozen exemplar at [`tests/fixtures/bazaar/json-api-snapshot.json`](./tests/fixtures/bazaar/json-api-snapshot.json), and a snapshot test at [`tests/integration/bazaar-check-json-api.test.ts`](./tests/integration/bazaar-check-json-api.test.ts) (6 tests catch field renames, removals, additions, and reordering against the exemplar). **Versioning rule** committed: additive changes (new optional fields, new check names, new `verdict.kind` values, new `detail.*` keys) ship in MINOR versions with a `### JSON API` CHANGELOG entry; shape-breaking changes (renames, removals, type changes, fixed-position reordering) require a MAJOR version + integrator notice. Exit-code contract preserved across all minor versions (D.3's `upstream_stuck` will roll up to exit code 3, not a new code). README "JSON API" section added under the `bazaar-check` documentation; `CONTRIBUTING.md` "JSON API discipline" section added with a PR self-check. TomSmart_ai's mapper-integration is the named consumer this contract is committed to.
- **`bazaar-check --endpoint <paid-url>` per-route 402 probe mode** ([X402-42](https://vahdatfardin.atlassian.net/browse/X402-42), D.4). For services that publish per-route instead of at root `/.well-known/x402` — the [#2207](https://github.com/x402-foundation/x402/issues/2207) pattern documented by @AsaiShota (test-echo-cdp), @evanatpizzarobot (TensorFeed), and @0xdespot (hyperD.ai). When `--endpoint` is supplied, the bazaar-check pipeline skips the root well-known probe entirely and fetches the 402 challenge directly from the given paid URL; manifest-shape validations run against the 402 body instead of the root manifest. Self-payment guard + CDP indexing query continue to run unchanged (they consume the challenge body's `payTo` regardless of how the challenge was obtained). UX: a one-line info note prints when `--endpoint` is used (`ℹ skipping root /.well-known/x402 probe per --endpoint. Note: services that DO publish at root signal extra discoverability hygiene; consider both.`); routes to stdout in `--log human` and stderr in `--log json` so stdout stays JSON-parseable. New `endpoint?: string` field on `BazaarCheckOptions` (programmatic API); new `endpoint?: string` field on `BazaarCheckCommandOptions`. URL-validated at command entry; non-URL values exit 1 with a clear error.
Expand All @@ -19,13 +20,18 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

- **`bazaar-check` false-positive `implementation_issue` on body-discovery services** ([#72](https://github.com/fardinvahdat/x402trace/issues/72) → [X402-43](https://vahdatfardin.atlassian.net/browse/X402-43), thanks @AsaiShota 🙏 for the diagnosis with `@x402/extensions@2.11.0` package-source citation, and @0xdespot 🙏 for the second-voice corroboration on [x402-foundation/x402#2207](https://github.com/x402-foundation/x402/issues/2207)). v0.3.0/0.3.1's `src/bazaar/challenge.ts` and `src/bazaar/well-known.ts` both validated `extensions.bazaar` assuming the **MCP-discovery** shape (top-level `name` + `description`). Real-world v2 services using the **Body-discovery** shape (`info.input` + `info.output` + `schema` per `declareDiscoveryExtension(...)`) failed the envelope check despite being correctly implemented — verdict overrode the substantive indexing-probe signal. New `src/bazaar/extensions-bazaar.ts` helper detects the variant by shape (body-discovery indicators win; else assume mcp-discovery per ADR-004) and validates the variant-appropriate required-field set. Default-deny third branch for future `@x402/extensions` variants we don't recognise yet — skips shape validation with an informational pass rather than false-positive. Tests cover ≥4 cases per affected file (challenge.ts + well-known.ts) plus 13 helper unit tests.

### Changed

- **`indexing` check's `detail.status` field gains a sibling `detail.indexer_state` field** ([X402-46](https://vahdatfardin.atlassian.net/browse/X402-46), D.3). The legacy `status` (`"indexed" \| "processing" \| "not_found" \| "error"`) is preserved for backward-compat; `indexer_state` (`"indexed" \| "processing" \| "unknown" \| "not_applicable_non_cdp"`) is the canonical facet for v0.3.2+ consumers. The `404` and `empty resources` cases that previously surfaced as `status: "not_found"` now ALSO carry `indexer_state: "processing"` — same upstream signal, sharper attribution per Max's polyodds.bet case and @0xdespot's bucket taxonomy from [#2207](https://github.com/x402-foundation/x402/issues/2207).

### JSON API

X402-44 establishes the formal `### JSON API` subsection discipline going forward — every PR that touches `--log json` output adds an entry here. v0.3.2-cycle changes so far:

- **D.5 additive:** `detail.variant` field on extensions.bazaar-related check results from the variant-aware refactor (value: `"mcp-discovery"`, `"body-discovery"`, or `"unknown"`).
- **D.4 additive:** when `--endpoint` is supplied, the `well-known` result shape is preserved with a different `message`: `{ check: "well-known", status: "pass", message: "skipped per --endpoint (probing <url> directly instead of /.well-known/x402)" }`. The envelope shape is preserved; the well-known slot's `message` field is the only carrier of the skip signal.
- **D.2 additive (X402-45):** the `results` array gains a 5th entry, `{ check: "propagation", status, message, detail }`. The `detail.metadata_propagation` enum carries the propagation diff state (`"ok" \| "partial" \| "missing" \| "unknown"`); when status is `info`, `detail.diff` contains per-field mismatch records (`{ field, manifest, indexer }`). The snapshot fixture at `tests/fixtures/bazaar/json-api-snapshot.json` was regenerated to capture the 5th result and now has a new "5 canonical checks in fixed order" invariant test (was 4).
- **D.3 additive (X402-46):** the `indexing` check gains a `detail.indexer_state` field with four values (`"indexed" \| "processing" \| "unknown" \| "not_applicable_non_cdp"`). The `metadata_propagation` enum on the `propagation` check gains a 5th value `"not_applicable_non_cdp"` for services that declare a non-CDP facilitator. A new top-level `verdict.kind` value `"upstream_stuck"` joins the discriminator union — same shape as `upstream_issue` (`{kind, message, exitCode: 3, upstreamChecks: string[]}`), distinct attribution. Snapshot fixture regenerated to capture the new `indexer_state` field on the indexing detail.
- The 4-check → 5-check expansion is the snapshot-test discipline working as designed: the snapshot test failed on the additive change, the maintainer regenerated the fixture, and this entry documents the intent. Exit-code contract preserved unchanged (D.2's `info` signals roll up to `upstream_issue` exit 3 just like the existing indexing check).

## [0.3.1] — 2026-05-20
Expand Down
8 changes: 7 additions & 1 deletion scripts/check-publish-surface.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ import { readFileSync, readdirSync, statSync, existsSync } from "node:fs";
import { join } from "node:path";

const MAX_UNPACKED_BYTES = 400 * 1024;
const MAX_FILES = 100;
// v0.3.0: 96 → v0.3.1 + PR #69 fixtures: 100 → v0.3.2 cycle adds (json-api
// stability + D.5 extensions-bazaar + D.2 propagation + D.3 facilitator-
// detect): 102. Raised to 110 with breathing room for X402-47 fixture
// consumption + future v0.3.x facets. Reduce again when the next major
// surface cleanup happens (e.g., extracting the legacy `detail.status`
// field on indexing).
const MAX_FILES = 110;
const DIST_DIR = "dist";

if (!existsSync(DIST_DIR)) {
Expand Down
69 changes: 69 additions & 0 deletions src/bazaar/facilitator-detect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* X402-46 (D.3) — shared facilitator detection helper.
*
* ADR-004 Pillar 3 establishes that Bazaar indexing is CDP-only by
* design (Cryptor + Ferj correction 2026-05-21). Non-CDP services
* (self-hosted facilitators, x402-rs, alternative providers) do NOT
* appear in CDP discovery — that's working-as-intended, not a bug.
*
* Both D.2 (propagation diff) and D.3 (indexer-state probe) need to
* answer: "is this service CDP-settled?" — so the verdict can return
* `not_applicable_non_cdp` (rolling up to `looks_correct`) instead of
* false-positive `upstream_issue` on a working non-CDP service.
*
* **v1 detection mechanism: manifest-claim only.** If the operator
* declares `extensions.bazaar.facilitator` in their well-known
* manifest, trust it. Three other options exist per ADR-004 Pillar 3
* (TomSmart's `facilitator_inferred` field, empirical probe, hybrid)
* but they require either external fixture data (still pending Sun
* 2026-05-24 drop) or additional HTTP calls. Manifest-claim is fast,
* synchronous, and correct for the operators who self-declare.
*
* **Default behavior is `unknown`** (not `cdp`) when the manifest
* doesn't declare. This is intentional: the upstream check path
* already handles "indexed in CDP discovery" vs "not indexed" — the
* facilitator-detect helper is for the case where the operator has
* explicitly told us they're non-CDP, and we should respect that
* declaration even before/without hitting the network.
*/

import type { WellKnownManifest } from "./types.js";

export type FacilitatorDetectionResult = "cdp" | "non-cdp" | "unknown";

/**
* Detect the facilitator a service settles through, based on its
* well-known manifest declaration.
*
* Recognised CDP claim values (case-insensitive): `"coinbase-cdp"`,
* `"cdp"`, `"coinbase"`. Any other non-empty string value of
* `extensions.bazaar.facilitator` is treated as non-CDP.
*
* Returns `unknown` when:
* - `manifest` is undefined (e.g., --endpoint mode skipped well-known)
* - `extensions.bazaar` is missing or not an object
* - `extensions.bazaar.facilitator` is missing, not a string, or
* whitespace-only
*
* The undefined-manifest case (`--endpoint` mode) returning `unknown`
* preserves the conservative path: if we don't have the manifest, we
* don't make an attribution. D.2 and D.3 already handle the unknown
* case correctly by returning their own `unknown` state.
*/
export function detectFacilitator(
manifest: WellKnownManifest | undefined,
): FacilitatorDetectionResult {
if (manifest === undefined) return "unknown";
const extensions = manifest.extensions;
if (typeof extensions !== "object" || extensions === null) return "unknown";
const bazaar = (extensions as Record<string, unknown>)["bazaar"];
if (typeof bazaar !== "object" || bazaar === null) return "unknown";
const claim = (bazaar as Record<string, unknown>)["facilitator"];
if (typeof claim !== "string") return "unknown";
const normalised = claim.trim().toLowerCase();
if (normalised === "") return "unknown";
if (normalised === "coinbase-cdp" || normalised === "cdp" || normalised === "coinbase") {
return "cdp";
}
return "non-cdp";
}
5 changes: 4 additions & 1 deletion src/bazaar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,15 @@ export async function runBazaarCheck(opts: BazaarCheckOptions): Promise<BazaarRe
});
}

// 4. Indexing query (CDP discovery) — only if we have a payTo from the challenge
// 4. Indexing query (CDP discovery) — only if we have a payTo from the challenge.
// X402-46 (D.3): manifest is passed for facilitator detection (short-circuits to
// `not_applicable_non_cdp` when the operator declares a non-CDP facilitator).
if (challengeFetch.ok) {
results.push(
await checkIndexing(challengeFetch.requirements.payTo, {
fetcher,
...(opts.discoveryBaseUrl !== undefined ? { discoveryBaseUrl: opts.discoveryBaseUrl } : {}),
...(manifest !== undefined ? { manifest } : {}),
}),
);
} else {
Expand Down
Loading