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
72 changes: 72 additions & 0 deletions .agents/plans/validate-author-route-pubkey.plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
name: validate-author-route-pubkey
overview: "Reject malformed Solana author route parameters before trust, dispute, and identity lookups can perform RPC or database work."
todos:
- id: confirm-route-boundary-gap
content: Confirm the current GET route accepts any non-EVM path value and identify the established chain-address validation helper
status: completed
- id: validate-solana-author-param
content: Validate non-EVM author route parameters before Solana trust, dispute, or identity lookups
status: completed
- id: add-no-side-effect-regression
content: Add a focused GET regression proving malformed Solana author parameters return 400 without downstream lookups
status: completed
- id: verify-focused-change
content: Run the targeted author route test plus format, lint, typecheck, full web tests, webpack build, and git whitespace checks
status: completed
isProject: false
---

# Validate Author Route Public Keys

## Goal
Make `GET /api/author/[pubkey]` reject a malformed non-EVM path parameter with a client-error response before it can invoke Solana trust, dispute, or identity helpers.

## Scope
- In scope: the public author GET boundary and its adjacent route test.
- Out of scope: author-trust semantics, Base/EVM lookup behavior, database schema, RPC configuration, wallet auth, and chain deployment work.

## Files To Change
- `web/app/api/author/[pubkey]/route.ts`: validate the Solana-default branch with the shared chain-address helper before downstream work.
- `web/__tests__/api/author-route.test.ts`: cover the malformed-Solana-param `400` contract and no-side-effect invariant.

## Verified Gap (2026-09-03)
- The route treats any non-EVM-shaped `pubkey` as the Solana path and immediately calls `resolveAuthorTrust`, `listAuthorDisputesByAuthor`, and `resolveAgentIdentityByWallet` (`web/app/api/author/[pubkey]/route.ts:88-100`).
- The shared `isValidChainAddress` helper already validates Solana addresses without adapter or network work (`web/lib/chainAddress.ts:62-72`), but this route does not use it.
- The route test covers successful Solana-style reads and EVM validation but has no malformed-Solana-path regression (`web/__tests__/api/author-route.test.ts:117-143`).
- As of 2026-09-03, no open PR changes these files; open PRs #157–#168 cover distinct request-body, ID, numeric-input, docs, and UI seams.

## Implementation Steps
1. Import the shared chain-address validation helper and configured Solana chain context.
2. After the EVM branch, reject a non-Solana-valid `pubkey` with a stable `400` response before any trust, dispute, or identity helper runs.
3. Add one test using an invalid author path parameter. Assert the exact response and zero calls to the three downstream lookup mocks.

## Verification
Run under the repository-required Node 24 environment:
```bash
. "$HOME/.nvm/nvm.sh" --no-use && { nvm use --silent || nvm install; }
npm test --workspace @agentvouch/web -- __tests__/api/author-route.test.ts --maxWorkers=1 --no-fileParallelism
npm run format:check
npm run lint:web
npm run typecheck
npm test --workspace @agentvouch/web -- --maxWorkers=1 --no-fileParallelism
npm exec --workspace @agentvouch/web -- next build --webpack
git diff --check
```

Acceptance criteria: a malformed Solana-default route parameter returns `400`, runs no downstream trust/dispute/identity lookup, valid Solana and Base author requests retain their contracts, and the full local web gate passes.

### Execution Note (2026-09-03)
- The route now calls `isValidChainAddress` with the configured Solana context before Solana trust, dispute, or identity lookups. Base/EVM routing remains unchanged.
- The focused test passed: `web/__tests__/api/author-route.test.ts` (7 tests).
- `npm run format:check`, `npm run lint:web`, and `npm run typecheck` passed. The full web suite passed (128 files, 928 tests), followed by a successful `npm exec --workspace @agentvouch/web -- next build --webpack` and `git diff --check`.
- The webpack build retained existing `ox`/`viem` dynamic-dependency warnings and expected static-generation `DATABASE_URL` fallback logs because local database credentials are absent. No live Solana RPC, browser, wallet, database, or deployment flow was run.

## Rollout
Ship as a focused public request-boundary hardening PR. It adds no schema, environment, money-flow, or chain deployment changes.

## Rollback
Revert the focused commit to restore the prior public-route behavior. No stored data or deployment state requires rollback.

## Blockers
- No live Solana RPC or browser flow will be run; this is verified through route-level behavior and no-side-effect tests.
20 changes: 18 additions & 2 deletions web/__tests__/api/author-route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,10 @@ describe("POST /api/author/[pubkey]", () => {
mockResolveIdentity.mockResolvedValue({ canonicalAgentId: "agent-1" });
mockListAuthorDisputes.mockResolvedValue([{ publicKey: "Dispute111" }]);

const req = new NextRequest("http://localhost/api/author/Author111");
const pubkey = "AGNtBjLEHFnssPzQjZJnnqiaUgtkaxj4fFaWoKD6yVdg";
const req = new NextRequest(`http://localhost/api/author/${pubkey}`);
const res = await GET(req, {
params: Promise.resolve({ pubkey: "Author111" }),
params: Promise.resolve({ pubkey }),
});
const body = await res.json();

Expand All @@ -142,6 +143,21 @@ describe("POST /api/author/[pubkey]", () => {
expect(body.author_disputes).toEqual([{ publicKey: "Dispute111" }]);
});

it("rejects malformed Solana author parameters before downstream lookups", async () => {
const res = await GET(
new NextRequest("http://localhost/api/author/not-a-solana-address"),
{ params: Promise.resolve({ pubkey: "not-a-solana-address" }) }
);

expect(res.status).toBe(400);
await expect(res.json()).resolves.toEqual({
error: "Solana author routes require a valid Solana address",
});
expect(mockResolveAuthorTrust).not.toHaveBeenCalled();
expect(mockListAuthorDisputes).not.toHaveBeenCalled();
expect(mockResolveIdentity).not.toHaveBeenCalled();
});

it("returns 400 when the selected discovered candidate does not belong to the wallet", async () => {
mockVerify.mockReturnValue({ valid: true, pubkey: "Author111" });
mockVerifyAuthorTrust.mockResolvedValue({ isRegistered: true });
Expand Down
14 changes: 14 additions & 0 deletions web/app/api/author/[pubkey]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import { discoverSolanaRegistryCandidatesByWallet } from "@/lib/solanaAgentRegis
import { listAuthorDisputesByAuthor } from "@/lib/authorDisputes";
import {
BASE_SEPOLIA_CHAIN_CONTEXT,
getConfiguredSolanaChainContext,
normalizeInputChainContext,
} from "@/lib/chains";
import { isValidChainAddress } from "@/lib/chainAddress";
import {
buildPublicCacheControl,
PUBLIC_ROUTE_CACHE_SECONDS,
Expand Down Expand Up @@ -85,6 +87,18 @@ export async function GET(
);
}

if (
!isValidChainAddress({
chainContext: getConfiguredSolanaChainContext(),
value: pubkey,
})
) {
return NextResponse.json(
{ error: "Solana author routes require a valid Solana address" },
{ status: 400 }
);
}

const authorTrust = await resolveAuthorTrust(pubkey);
const authorDisputes = await listAuthorDisputesByAuthor(pubkey);
let authorIdentity = null;
Expand Down
Loading