livepeer / livepeer/explorer

Add zod validation in more places

Open
#434 2 comments 0 reactions 1 assignee View on GitHub

@Roaring30s is already working on this.

Since Jan 17, 2026.

  • #612 by @ECWireless — open
Dominant language
TypeScript
Stars
11
Forks
26
Avg merge
11h 58m
Merged PRs (30d)
16

Description

### Is your feature request related to a problem?

Many data fetches are not handling errors, nor validating data types.

### Describe the solution you'd like

This issue identifies areas where Zod schemas can be used to validate:
1. **API endpoint inputs** (query parameters, request bodies)
2. **API endpoint return values** (response data)
3. **External API responses** (third-party API data)

## Current State

- Zod is already installed (`zod@^4.1.12` in package.json)
- Zod is currently used in `pages/api/usage.tsx` to validate external API responses (`DayDataSchema`)
- Basic address validation exists via `isValidAddress()` helper, but could be enhanced with Zod

---

## 1. API Endpoint Input Validation

### 1.1 Address-based Endpoints

These endpoints accept an `address` query parameter that should be validated:

#### `/api/account-balance/[address].tsx`
- **Input**: `address` query param
- **Current validation**: `isValidAddress(address)` - basic string check
- **Zod opportunity**: Create `AddressSchema = z.string().regex(/^0x[a-fA-F0-9]{40}$/)` for stricter validation

#### `/api/ens-data/[address].tsx`
- **Input**: `address` query param
- **Current validation**: `isValidAddress(address)` + blacklist check
- **Zod opportunity**: Same as above, plus validate blacklist separately

#### `/api/score/[address].tsx`
- **Input**: `address` query param
- **Current validation**: `isValidAddress(address)`
- **Zod opportunity**: Address validation schema

#### `/api/pending-stake/[address].tsx`
- **Input**: `address` query param
- **Current validation**: `isValidAddress(address)`
- **Zod opportunity**: Address validation schema

#### `/api/l1-delegator/[address].tsx`
- **Input**: `address` query param
- **Current validation**: `isValidAddress(address)`
- **Zod opportunity**: Address validation schema

### 1.2 Treasury/Proposal Endpoints

#### `/api/treasury/proposal/[proposalId]/state.tsx`
- **Input**: `proposalId` query param (string)
- **Current validation**: Basic existence check `if (!proposalId)`
- **Zod opportunity**:
```typescript
const ProposalIdSchema = z.string().regex(/^\d+$/) // numeric string
```

#### `/api/treasury/proposal/[proposalId]/votes/[address].tsx`
- **Input**:
- `proposalId` query param
- `address` query param
- **Current validation**: Basic checks
- **Zod opportunity**: Combined schema for both params

#### `/api/treasury/votes/[address]/index.tsx`
- **Input**: `address` query param
- **Current validation**: `isValidAddress(address)`
- **Zod opportunity**: Address validation schema

#### `/api/treasury/votes/[address]/registered.tsx`
- **Input**: `address` query param
- **Current validation**: `isValidAddress(address)`
- **Zod opportunity**: Address validation schema

### 1.3 POST Endpoints with Request Bodies

#### `/api/upload-ipfs.tsx`
- **Input**: `req.body` (JSON object)
- **Current validation**: None - directly passes `req.body` to external API
- **Zod opportunity**:
```typescript
const IpfsUploadSchema = z.object({
// Define expected structure of IPFS upload data
// This depends on what data is actually being uploaded
})
```

#### `/api/generateProof.tsx`
- **Input**: `req.body` with `{ account, delegate, stake, fees }`
- **Current validation**: None - directly destructures from `req.body`
- **Zod opportunity**:
```typescript
const GenerateProofSchema = z.object({
account: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
delegate: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
stake: z.string(), // or z.bigint() if needed
fees: z.string(), // or z.bigint() if needed
})
```

### 1.4 Query Parameters with Optional Values

#### `/api/pipelines/index.tsx`
- **Input**: Optional `region` query param
- **Current validation**: None
- **Zod opportunity**:
```typescript
const PipelinesQuerySchema = z.object({
region: z.string().optional(),
})
```

#### `/api/score/index.tsx`
- **Input**: Optional `pipeline` and `model` query params
- **Current validation**: None
- **Zod opportunity**:
```typescript
const ScoreQuerySchema = z.object({
pipeline: z.string().optional(),
model: z.string().optional(),
})
```

---

## 2. API Endpoint Return Value Validation

All API endpoints return typed data, but there's no runtime validation. Adding Zod schemas would catch contract/API changes early.

### 2.1 Account/Balance Endpoints

#### `/api/account-balance/[address].tsx`
- **Return type**: `AccountBalance`
- **Zod opportunity**:
```typescript
const AccountBalanceSchema = z.object({
balance: z.string(),
allowance: z.string(),
})
```

#### `/api/pending-stake/[address].tsx`
- **Return type**: `PendingFeesAndStake`
- **Zod opportunity**:
```typescript
const PendingFeesAndStakeSchema = z.object({
pendingStake: z.string(),
pendingFees: z.string(),
})
```

#### `/api/l1-delegator/[address].tsx`
- **Return type**: `L1Delegator`
- **Zod opportunity**:
```typescript
const UnbondingLockSchema = z.object({
id: z.number(),
amount: z.string(),
withdrawRound: z.string(),
})

const L1DelegatorSchema = z.object({
delegateAddress: z.string(),
pendingStake: z.string(),
pendingFees: z.string(),
transcoderStatus: z.enum(["not-registered", "registered"]),
unbondingLocks: z.array(UnbondingLockSchema),
activeLocks: z.array(UnbondingLockSchema),
})
```

### 2.2 ENS Endpoints

#### `/api/ens-data/[address].tsx`
- **Return type**: `EnsIdentity`
- **Zod opportunity**:
```typescript
const EnsIdentitySchema = z.object({
id: z.string(),
idShort: z.string(),
avatar: z.string().nullable().optional(),
name: z.string().nullable().optional(),
url: z.string().nullable().optional(),
twitter: z.string().nullable().optional(),
github: z.string().nullable().optional(),
description: z.string().nullable().optional(),
})
```

### 2.3 Round/Protocol Endpoints

#### `/api/current-round.tsx`
- **Return type**: `CurrentRoundInfo`
- **Zod opportunity**:
```typescript
const CurrentRoundInfoSchema = z.object({
id: z.number(),
startBlock: z.number(),
initialized: z.boolean(),
currentL1Block: z.number(),
currentL2Block: z.number(),
})
```

### 2.4 Performance/Score Endpoints

#### `/api/score/[address].tsx`
- **Return type**: `PerformanceMetrics`
- **Zod opportunity**:
```typescript
const RegionalValuesSchema = z.record(z.string(), z.number())

const ScoreSchema = z.object({
value: z.number(),
region: z.string(),
model: z.string(),
pipeline: z.string(),
orchestrator: z.string(),
})

const PerformanceMetricsSchema = z.object({
successRates: RegionalValuesSchema,
roundTripScores: RegionalValuesSchema,
scores: RegionalValuesSchema,
pricePerPixel: z.number(),
topAIScore: ScoreSchema,
})
```

#### `/api/score/index.tsx`
- **Return type**: `AllPerformanceMetrics`
- **Zod opportunity**:
```typescript
const AllPerformanceMetricsSchema = z.record(
z.string(),
PerformanceMetricsSchema
)
```

### 2.5 Treasury Endpoints

#### `/api/treasury/proposal/[proposalId]/state.tsx`
- **Return type**: `ProposalState`
- **Zod opportunity**:
```typescript
const ProposalStateSchema = z.object({
id: z.string(),
state: z.enum([
"Pending",
"Active",
"Canceled",
"Defeated",
"Succeeded",
"Queued",
"Expired",
"Executed",
"Unknown",
]),
quota: z.string(),
quorum: z.string(),
totalVoteSupply: z.string(),
votes: z.object({
against: z.string(),
for: z.string(),
abstain: z.string(),
}),
})
```

#### `/api/treasury/votes/[address]/index.tsx`
- **Return type**: `VotingPower`
- **Zod opportunity**:
```typescript
const VotingPowerSchema = z.object({
proposalThreshold: z.string(),
self: z.object({
address: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
votes: z.string(),
}),
delegate: z.object({
address: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
votes: z.string(),
}).optional(),
})
```

#### `/api/treasury/votes/[address]/registered.tsx`
- **Return type**: `RegisteredToVote`
- **Zod opportunity**:
```typescript
const RegisteredToVoteSchema = z.object({
registered: z.boolean(),
delegate: z.object({
address: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
registered: z.boolean(),
}),
})
```

#### `/api/treasury/proposal/[proposalId]/votes/[address].tsx`
- **Return type**: `ProposalVotingPower`
- **Zod opportunity**:
```typescript
const ProposalVotingPowerSchema = z.object({
self: z.object({
address: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
votes: z.string(),
hasVoted: z.boolean(),
}),
delegate: z.object({
address: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
votes: z.string(),
hasVoted: z.boolean(),
}).optional(),
})
```

### 2.6 Pipeline/Region Endpoints

#### `/api/pipelines/index.tsx`
- **Return type**: `AvailablePipelines`
- **Zod opportunity**:
```typescript
const PipelineSchema = z.object({
id: z.string(),
models: z.array(z.string()),
regions: z.array(z.string()),
})

const AvailablePipelinesSchema = z.object({
pipelines: z.array(PipelineSchema),
})
```

#### `/api/regions/index.ts`
- **Return type**: `Regions`
- **Zod opportunity**:
```typescript
const RegionSchema = z.object({
id: z.string(),
name: z.string(),
type: z.enum(["transcoding", "ai"]),
})

const RegionsSchema = z.object({
regions: z.array(RegionSchema),
})
```

### 2.7 Usage/Chart Endpoints

#### `/api/usage.tsx`
- **Return type**: `HomeChartData`
- **Current validation**: Already uses `DayDataSchema` for external API response
- **Zod opportunity**: Create schema for the full `HomeChartData` return type

#### `/api/upload-ipfs.tsx`
- **Return type**: `AddIpfs`
- **Zod opportunity**:
```typescript
const AddIpfsSchema = z.object({
hash: z.string(),
})
```

---

## 3. External API Response Validation

These endpoints fetch data from external APIs and should validate responses before using them.

### 3.1 Metrics/AI Server Responses

#### `/api/score/[address].tsx`
- **External APIs**:
1. `NEXT_PUBLIC_AI_METRICS_SERVER_URL/api/top_ai_score` → `ScoreResponse`
2. `NEXT_PUBLIC_METRICS_SERVER_URL/api/aggregated_stats` → `MetricsResponse`
3. Pricing URL → `PriceResponse`
- **Current validation**: None - directly uses `await response.json()`
- **Zod opportunity**:
```typescript
const ScoreResponseSchema = z.object({
value: z.number(),
region: z.string(),
model: z.string(),
pipeline: z.string(),
orchestrator: z.string(),
})

const MetricSchema = z.object({
success_rate: z.number(),
round_trip_score: z.number(),
score: z.number(),
})

const MetricsResponseSchema = z.record(
z.string(),
z.record(z.string(), MetricSchema).optional()
)

const PriceResponseSchema = z.array(z.object({
Address: z.string(),
ServiceURI: z.string(),
LastRewardRound: z.number(),
RewardCut: z.number(),
FeeShare: z.number(),
DelegatedStake: z.string(),
ActivationRound: z.number(),
DeactivationRound: z.string(),
Active: z.boolean(),
Status: z.string(),
PricePerPixel: z.number(),
UpdatedAt: z.number(),
}))
```

#### `/api/score/index.tsx`
- **External APIs**: Same as above
- **Current validation**: None
- **Zod opportunity**: Same schemas as above

#### `/api/pipelines/index.tsx`
- **External API**: `NEXT_PUBLIC_AI_METRICS_SERVER_URL/api/pipelines`
- **Current validation**: None - directly uses `await response.json()`
- **Zod opportunity**: Use `AvailablePipelinesSchema` (defined in section 2.6)

#### `/api/regions/index.ts`
- **External APIs**:
- `NEXT_PUBLIC_METRICS_SERVER_URL/api/regions`
- `NEXT_PUBLIC_AI_METRICS_SERVER_URL/api/regions`
- **Current validation**: None
- **Zod opportunity**: Use `RegionsSchema` (defined in section 2.6)

### 3.2 Livepeer.com API

#### `/api/usage.tsx`
- **External API**: `https://livepeer.com/data/usage/query/total`
- **Current validation**: ✅ Already uses `DayDataSchema` with `safeParse()`
- **Status**: Good example of proper validation

### 3.3 Pinata IPFS API

#### `/api/upload-ipfs.tsx`
- **External API**: `https://api.pinata.cloud/pinning/pinJSONToIPFS`
- **Current validation**: None - directly uses `await fetchResult.json()`
- **Zod opportunity**:
```typescript
const PinataResponseSchema = z.object({
IpfsHash: z.string(),
// Add other fields if Pinata returns more
})
```

### 3.4 ENS Provider Responses

#### `lib/api/ens.ts` → `getEnsForAddress()`
- **External API**: Ethereum provider ENS lookups
- **Current validation**: None - directly uses provider responses
- **Zod opportunity**: Validate ENS resolver text records and avatar URLs

---

## 4. SSR/Client-Side API Calls

### 4.1 Server-Side Rendering

#### `lib/api/ssr.ts` → `getEnsIdentity()`
- **Internal API call**: `/api/ens-data/${address}`
- **Current validation**: None - directly uses `await response.json()`
- **Zod opportunity**: Use `EnsIdentitySchema` to validate the response

---

## 5. Recommended Implementation Strategy

### Phase 1: Shared Schemas
1. Create `lib/api/schemas/` directory
2. Create shared schemas for common types:
- `address.ts` - Address validation
- `common.ts` - Common types (strings, numbers, etc.)
- `ens.ts` - ENS-related schemas
- `treasury.ts` - Treasury/proposal schemas
- `performance.ts` - Performance metrics schemas

### Phase 2: Input Validation
1. Add input validation to all endpoints with query params
2. Add request body validation to POST endpoints
3. Return proper 400 errors with validation details

### Phase 3: Output Validation
1. Add return value validation to all endpoints
2. Log validation errors (don't fail in production, but log for monitoring)
3. Consider failing in development mode to catch issues early

### Phase 4: External API Validation
1. Validate all external API responses
2. Use `safeParse()` to handle validation errors gracefully
3. Add retry logic or fallback values where appropriate

### Example Implementation Pattern

```typescript
// lib/api/schemas/address.ts
import { z } from "zod";

export const AddressSchema = z.string().regex(/^0x[a-fA-F0-9]{40}$/);

// lib/api/schemas/account-balance.ts
import { z } from "zod";

export const AccountBalanceSchema = z.object({
balance: z.string(),
allowance: z.string(),
});

// pages/api/account-balance/[address].tsx
import { AddressSchema } from "@lib/api/schemas/address";
import { AccountBalanceSchema } from "@lib/api/schemas/account-balance";

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
// Validate input
const addressResult = AddressSchema.safeParse(req.query.address);
if (!addressResult.success) {
return res.status(400).json({ error: "Invalid address", details: addressResult.error });
}

// ... fetch data ...

// Validate output
const result = AccountBalanceSchema.safeParse(accountBalance);
if (!result.success) {
console.error("Account balance validation failed:", result.error);
// In production, might still return the data, but log the error
// In development, could return 500 to catch issues early
}

return res.status(200).json(accountBalance);
};
```

---

## Summary

**Total Opportunities:**
- **Input Validation**: ~15 endpoints need query param/body validation
- **Output Validation**: ~20 endpoints need return value validation
- **External API Validation**: ~8 external API calls need response validation

**Priority:**
1. **High**: External API responses (can cause runtime errors)
2. **Medium**: POST request bodies (security/data integrity)
3. **Medium**: Return value validation (catch contract changes)
4. **Low**: Query parameter validation (basic checks exist)

**Estimated Impact:**
- Better error messages for invalid inputs
- Early detection of API contract changes
- Protection against malformed external API responses
- Improved type safety at runtime

### Describe alternatives you've considered

_No response_

### Additional context

_No response_

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.