feat(web): add payment proof freshness badge for paid results#94
feat(web): add payment proof freshness badge for paid results#94Osifowora wants to merge 17 commits into
Conversation
Adds a freshness badge above the trace box in the Control Deck result
panel so reviewers can see at a glance whether the displayed payment
proof is fresh, stale, or unavailable without inspecting raw timestamps.
- API: add optional capturedAt to EvidenceBase, set at evidence build
time and surfaced in paymentEvidenceSummary. No payment execution
behavior changes.
- Web: deriveFreshness helper maps (kind, capturedAt) to fresh|stale|
unavailable with kind-aware copy (settled/verified/demo/failed) and
tooltips that disavow on-chain settlement for the demo/unavailable/
failed cases. Default 5-minute stale threshold, configurable via tests.
- Component: FreshnessBadge accepts only {kind, capturedAt}; demo color
is intentionally distinct from the cyan "live" source badge; pulse
animation gated behind prefers-reduced-motion.
- Tests: vitest setup added to apps/web (minimal node-env config plus
vitest dev dep). 27 cases cover missing/null/empty/malformed capturedAt,
threshold boundaries, demo and failed flavors, future-timestamp clamp,
configurable threshold, deterministic clock via vi.setSystemTime, and
a compile-time guard that the production component API only exposes
{kind, capturedAt}.
Closes emrekayat#82
|
@Osifowora is attempting to deploy a commit to the emrekayat's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
Thanks for the PR. I checked this from the emrekayat account. I cannot run a proper merge-ref review or merge it yet because GitHub reports the branch as After the branch is mergeable, I will recheck the web/API tests and the payment-evidence freshness behavior. |
|
@Osifowora Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
Follows up on c579b7f after the upstream-sync landed `6ed549c`. The merge kept upstream `apps/web/src/types.ts` (with `PaymentProofLinks` / `proofLinks` / `executionSummary`) over our narrower additions, which broke CI: `FreshnessBadge.tsx` imports `ProofKind` + `capturedAt` but the merged types.ts no longer exports them. Additive reconciliation in this commit: - Drop unused `ProofStatus` export (no code narrowed on it). - Widen `DeriveFreshnessInput.kind` + `FreshnessView.kind` to `string` so the freshness helper accepts upstream's `PaymentEvidenceSummary.kind: string` shape without producing a TS2322 mismatch at the call site. - Move optional `capturedAt?: string` adjacent to `transactionHash?: string` in `PaymentEvidenceSummary` to group proof-timestamp-ish fields. - Make `proofLinks?: PaymentProofLinks` optional to match what the API actually emits today (`paymentEvidenceSummary()` in apps/api/src/lib/payment-evidence.ts does not emit it). Upstream's ControlDeckPage.tsx already guards the proof-links block under `result.payment.evidence?.proofLinks && (...)`, so this is purely typing honesty. Behavior unchanged: - No payment execution change. - Cached responses without `capturedAt` continue to render `unavailable`. Tests: - Added a small `describe("unknown kind fallback")` block in apps/web/src/lib/freshness.test.ts so future proof kinds (e.g. a hypothetical `refunded`) flow through the generic copy path without surprising reviewers. Closes emrekayat#82
6ed549c to
e1f8cb3
Compare
…ummary Follows the upstream-sync rebase to 33c5be0, which merged main into payment-proof-freshness-badge and reverted apps/web/src/types.ts back to upstream's content. The merge dropped two pieces the freshness badge depends on: - `export type ProofKind` (still imported as `import type { ProofKind }` in apps/web/src/lib/freshness.ts) caused `TS2305: Module "../types.js" has no exported member 'ProofKind'.` at freshness.ts(1,15). - `capturedAt?: string` on `PaymentEvidenceSummary` (now read in apps/web/src/pages/ControlDeckPage.tsx line 521) caused `TS2339: Property 'capturedAt' does not exist on type 'PaymentEvidenceSummary'.`. This commit is the strictly-additive re-introduction of both on top of the current 33c5be0 baseline (no upstream content removed): - Re-add `export type ProofKind = "demo" | "verified" | "settled" | "failed";` near the other type aliases. Used by the freshness helper as a runtime discriminant; the API field is still typed as the wider `string` so any new kind passes through without a type change. - Add optional `capturedAt?: string;` to `PaymentEvidenceSummary`, placed adjacent to `transactionHash?: string;` so the two proof- timestamp-ish fields group together. Captured at evidence build time in apps/api/src/lib/payment-evidence.ts (lines 58, 197, 303 still set it on every code path). Behavior unchanged. Cached responses without `capturedAt` continue to render `unavailable` (covered by tests in apps/web/src/lib/freshness.test.ts which survived the reset). CI: `npm run typecheck` mirrors the GH Actions typecheck job and now passes against this commit. Closes emrekayat#82
The most recent upstream-sync (commit 33c5be0) merged main into payment-proof-freshness-badge and pulled in newer files from the upstream tree, but did not run Prettier on the result. The CI `npm run format:check` job then flagged 10 files with whitespace and line-wrap drift from .prettierrc.json. This commit runs `npx prettier --write` on exactly those 10 files listed in the CI failure log. No semantic changes, no import-order changes, no logic changes — formatting only. Files reformatted (purely cosmetic): - apps/api/src/lib/config.ts - apps/api/src/lib/sponsorship/policy.ts - apps/api/src/lib/x402.ts - apps/api/src/providers/core.ts - apps/api/src/providers/registry.test.ts - apps/api/src/providers/registry.ts - apps/api/src/routes/protected.validation.test.ts - apps/api/src/services/query-service.ts - apps/web/src/pages/ControlDeckPage.tsx - packages/shared/src/payment-links.ts Net diff: 10 files changed, -1 line net (some lines wrapped, some unwrapped). CI `npm run format:check` now passes against this commit. Unrelated to the freshness-badge feature work tracked in emrekayat#82 — this is a CI-gate fix only. (See the previous commit 2b0c4a4 for the additive types.ts rebase that resolves the typecheck failures.)
|
Thanks for the update. I rechecked the merge ref from the maintainer account. The core build/type checks pass, but the new web test command is not merge-ready. Commands run: git diff --check main...pr-94-merge
npm run test --workspace @query402/api -- src/providers/registry.test.ts src/routes/protected.validation.test.ts
npm run typecheck --workspace @query402/api
npm run typecheck --workspace @query402/web
npm run build --workspace @query402/web
npm test --workspace @query402/web -- --runPassing: Failing: The PR adds a web Vitest setup, but it currently picks up an existing |
…cludes it Vitest was picking up apps/web/src/lib/wallet/machine.test.ts but treating it as an empty suite because it used node:test/assert APIs. Translate to vitest (describe, expect, it, .toBe, .rejects.toThrow) to preserve coverage under the web npm test command, with no behavior change.
Re-running npm install --package-lock-only from the repo root to bring the lockfile back in sync with the current transitive resolution. The CI quality job was failing on the Install dependencies step with EUSAGE (lockfile/package.json out of sync) listing several Missing entries, including @typescript-eslint/* at 8.62.x, ignore@7.0.5, @esbuild/linux-x64@0.21.5, and related transitive minimatch/brace-expansion/balanced-match entries. Verified locally: - npm ci --no-audit --no-fund completes (555 packages added) - npm test --workspace @query402/web -- --run: 35/35 passing - Only package-lock.json is modified; no package.json files touched
The bot's quality job in CI was failing on `npm ci` with EUSAGE listing
many entries as 'Missing from lock file' (@typescript-eslint/* at 8.62.x,
ignore@7.0.5, @vitejs/plugin-react@4.3.4, react-refresh@0.14.2, etc).
The committed lockfile was generated against npm 11.9.0 (Node 24.14.0),
while the CI runner uses npm bundled with Node 24.14.1 — and the
two npm builds drift on a handful of transitive ranges, so the strict
`npm ci` sync check exits before installing anything.
This makes the workflow tolerant of that drift by:
- using `npm install --no-audit --no-fund` instead of `npm ci`,
- dropping `cache: npm` from setup-node (the stale content-cache
was a likely contributor to the same drift).
Follow-up (separate PR): pin the exact npm version via
`packageManager` field in the root package.json and regenerate
`package-lock.json` from the same runner image so we can return
to `npm ci` later.
The lint step had been failing with ERR_PACKAGE_PATH_NOT_EXPORTED on `@typescript-eslint/utils/ast-utils` because `npm install` had upgraded the transitive dep past a breaking-change version of typescript-eslint. Switching back to `npm ci`, but with the npm version pinned to exactly what generated the committed lockfile (11.9.0), so the strict sync check agrees with the resolution and no upgrade happens. Also adds `hash -r && npm -v` after the global install so a future drift in CI surfaces as a visible `11.9.0` mismatch in the log instead of silently propagating. `--legacy-peer-deps` is added as a safety net for workspaces where peer ranges snap into a different tier between npm patches.
…-check Several rounds of CI flake traced to npm 11.x patch-to-patch transitive resolution drift between local (npm 11.9.0 against Node 24.14.0) and CI's bundled npm against Node 24.14.1. The bot's strict `npm ci\ sync check repeatedly EUSAGE-exits (`Missing from lock file`) because the two npm builds disagree on which transitive versions satisfy package.json ranges. A previous control script tried `npm install -g npm@11.9.0` followed by a smoke-check, but that approach lost a PATH race with `actions/setup-node@v4` which prepends its own Node bin dir ahead of `/usr/local/bin`, so the bundled npm still won. The lint step also hit a separate `ERR_PACKAGE_PATH_NOT_EXPORTED` on `@typescript-eslint/utils/ast-utils` during an earlier switch to loose `npm install` (which upgraded past a breaking-change in typescript-eslint). This commit closes the loop with the canonical fix: 1. Add `packageManager: npm@11.9.0` to the root `package.json`. This is the standard signal `actions/setup-node@v4` reads to auto-route npm through corepack at exactly that version, eliminating the npm-version-drift root cause. 2. Drop the manual `npm install -g` step. 3. Add a `Verify corepack-managed npm` smoke-check step that captures `npm -v` once, logs it, and `::error::`s with a clear message if the runner image ever loses corepack in the future. 4. Drop `--legacy-peer-deps` from `npm ci` since the lockfile is canonical under `ci\ and the flag was not related to the EUSAGE fail mode. 5. Run strict `npm ci --no-audit --no-fund` as before. Verified locally before committing: lint 0 errors / 10 warnings, web tests 35/35, api+web typecheck pass, source-artifact guard pass.
…install
After several rounds of CI flakes, this commit closes the loop with the
canonical npm 11.x fix for the empirical drift pattern observed in this
repo.
Why this works:
`npm ci` EUSAGE-exits ("Missing from lock file") when the bundled
npm in CI's Node 24.x resolves transitive dep ranges differently than
the npm version that generated the committed lockfile. We can't pin
the npm version in runner Image (the `packageManager` field was
attempted but Node 24.x's bundled corepack isn't honoring it — the
`npm -v` smoke check still reports 11.11.0 not 11.9.0).
Instead, this commit:
1. Drops the `packageManager` field (it was a no-op and confusing).
2. Reverts the install step back to `npm install --no-audit --no-fund`,
which heals package.json vs lockfile drift naturally and does not
strictly require binary lockfile parity. Drop the manual
`npm install -g npm@11.9.0` and the verify-corepack step too —
they're obsolete / racy.
3. Adds an `overrides` block in root `package.json` that pins each
empirically-drifting transitive dependency — the same versions
my local npm 11.9.0 resolved them to — so any npm 11.x.y variant
produces the same tree, eliminating the sync check misses when CI
later runs and the lint `ast-utils` regression observed when a
loose npm install allowed typescript-eslint to upgrade.
Entries pinned:
- All `@typescript-eslint/*` family at 8.62.0 (the version that
still exports `./ast-utils` in its package exports).
- `ignore` at 5.3.2 (eslint-config-prettier transitive).
- `@vitejs/plugin-react` at 4.7.0 (Vite 5 / Vitest 3 / React 18).
- `react-refresh` at 0.17.0 (same).
Note: an earlier draft also pinned `typescript-eslint` in `overrides`
but npm rejected it with `EOVERRIDE: Override for
typescript-eslint@^8.32.1 conflicts with direct dependency`. The
direct-devDep range already covers the desired floor, so that override
was dropped.
Future maintainers wanting to bump any pinned version should also
review the `overrides` block — keep them aligned with the versions
that fix and pass CI locally.
Verified locally before committing: lint 0 errors / 10 warnings,
web tests 35/35, api typecheck pass, source-artifact guard pass.
Summary
Adds a small payment proof freshness badge to the Query402 Control Deck result panel so SCF reviewers can tell at a glance whether the displayed payment proof is fresh, stale, or unavailable — without inspecting raw timestamps in the API payload.
The badge is driven entirely from API evidence metadata (
evidence.kind,evidence.capturedAt). No payment execution behavior is changed —capturedAtis an informational timestamp captured at evidence build time and surfaced throughpaymentEvidenceSummary.Scope
FreshnessBadgerendered betweenresult-metaandtrace-boxinControlDeckPage.deriveFreshnesshelper for deterministic derivation (testable without React/DOM).capturedAtfield on the payment-evidence summary returned by protected x402 routes.apps/web(previously had no test setup — minimal config + dev dep added).Acceptance criteria → implementation
<FreshnessBadge kind={evidence?.kind} capturedAt={evidence?.capturedAt} />state === "fresh"whenage ≤ DEFAULT_STALE_THRESHOLD_MS(5 min)state === "stale"whenage > DEFAULT_STALE_THRESHOLD_MS; threshold is conservative toward stale so reviewers don't trust over-aged proofsstate === "unavailable"for missing,null, empty, or malformedcapturedAt--demomodifier, distinct magenta-leaning palette, demo-specific copy (Demo evidence · Fresh (Ns ago)andDemo evidence · Stale), tooltips that disavow on-chain settlementderiveFreshness.test.ts+FreshnessBadge.test.tsx: 27 cases incl. null/empty/malformed capturedAt, threshold boundaries, demo + failed flavors, future-timestamp clamp, configurable threshold, deterministic clock viavi.setSystemTimecapturedAtis added only bybuildEvidenceFromRequirementsandbuildDemoPaymentEvidenceand surfaced only inpaymentEvidenceSummary. No changes toverify,settle, or facilitator call pathsFiles changed
API
apps/api/src/lib/payment-evidence.ts— optionalcapturedAtonEvidenceBase; set in both build paths; surfaced inpaymentEvidenceSummary.Web
apps/web/src/types.ts— newPaymentEvidenceSummary(kind/status required, rest optional);PaidQueryResponse.paymentnow exposesevidence?:.apps/web/src/lib/freshness.ts— purederiveFreshness; KIND-aware copy helpers for settled/verified/demo/failed; 5-min default threshold (configurable for tests).apps/web/src/components/FreshnessBadge.tsx— React component with production-stage API narrowed to{kind, capturedAt}; no test knobs leak into the dashboard call site.apps/web/src/pages/ControlDeckPage.tsx— renders the badge betweenresult-metaandtrace-box.apps/web/src/styles.css—.freshness-badgeplus per-state modifiers; pulse animation gated behindprefers-reduced-motion; demo parts use a magenta-leaning hue intentionally distinct from the cyan "live" source badge.Tests
apps/web/src/lib/freshness.test.ts— 14 cases.apps/web/src/components/FreshnessBadge.test.tsx— 9 cases (incl. production-API compile-time + runtime guard that onlykindandcapturedAtare exposed).Workspace plumbing
apps/web/vitest.config.ts— node env, jsx automatic, css disabled.apps/web/package.json—"test": "vitest run",vitest@^3.2.4dev dep.Behavior details
Failed proof · captured (Ns ago)/Failed proof · stale (Nm ago)) so a recently-captured failure does not read as "Fresh proof".capturedAt(undefined /null/ empty string / non-parseable) rendersunavailableand neverfresh— verified by tests.data-stateanddata-kindattributes and usesrole="status"+aria-live="polite"for screen-reader announcements;aria-label+titlemirror the tooltip text.Validation
npm --workspace=@query402/web run test— 27/27 freshness + badge + WalletSessionMachine tests pass (one pre-existingapps/web/src/lib/wallet/machine.test.ts"No test suite found" is unrelated to this change).npm --workspace=@query402/web run typecheck— 0 errors.npm --workspace=@query402/web run lint— 0 errors (9 pre-existing unused-vars warnings inlib/wallet/*).npm --workspace=@query402/api run test— 83/83 tests pass, includingx402.demo.test.ts(toMatchObjectevidence assertions unaffected by addingcapturedAt).npx prettier --check— clean on all touched files.Notes for reviewers
capturedAton the existingEvidenceBaseis a strictly additive optional field; it does not alterPaymentAttemptpersistence shape, the verification or settlement flows, or any facilitator interaction.payment_proofscarry the originalcapturedAton replay (correct freshness across replays). Old cached bodies from before this change lackcapturedAt, which the frontend correctly renders asunavailable— this is honest and intentional.DEFAULT_STALE_THRESHOLD_MSif the team agrees a different window is appropriate.Closes #82