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
59 changes: 59 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,65 @@ curl http://localhost:3000/api/trust/not-an-address

---

### `GET /api/attestations/:address`

Returns persisted attestations for a subject address. Results are ordered newest
first and paginated with `page` and `limit`.

```
GET /api/attestations/:address?page=1&limit=20
```

**Response `200`**

```json
{
"address": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
"attestations": [
{
"id": 42,
"bondId": 10,
"attesterAddress": "0x2222222222222222222222222222222222222222",
"subjectAddress": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
"score": 90,
"note": "{\"key\":\"kyc\",\"value\":\"verified\"}",
"createdAt": "2025-01-01T00:00:00.000Z"
}
],
"offset": 0,
"page": 1,
"limit": 20,
"total": 1,
"hasNext": false
}
```

### `POST /api/attestations`

Creates a persisted attestation, invalidates attestation caches, and emits an
`attestation.created` outbox event.

```json
{
"bondId": 10,
"attesterAddress": "0x2222222222222222222222222222222222222222",
"subject": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
"key": "kyc",
"value": "verified",
"score": 90
}
```

**Responses**

| Status | Condition |
| ------ | --------- |
| `201` | Attestation persisted |
| `400` | Invalid address, score, pagination, or oversized `key`/`value` |
| `409` | Duplicate `(bondId, attesterAddress, subject)` attestation |

---

### `GET /api/bond/:address`

Returns bond status for an Ethereum address from the database.
Expand Down
135 changes: 135 additions & 0 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,143 @@ info:
description: Generated OpenAPI documentation from Zod schemas
servers:
- url: https://api.credence.org/v1
paths:
/api/attestations/{address}:
get:
summary: List attestations for an address
parameters:
- name: address
in: path
required: true
schema:
type: string
- name: page
in: query
required: false
schema:
type: integer
minimum: 1
default: 1
- name: limit
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 20
responses:
"200":
description: Paginated attestation list
content:
application/json:
schema:
type: object
required:
- address
- attestations
- offset
- page
- limit
- total
- hasNext
properties:
address:
type: string
attestations:
type: array
items:
$ref: "#/components/schemas/Attestation"
offset:
type: integer
page:
type: integer
limit:
type: integer
total:
type: integer
hasNext:
type: boolean
"400":
description: Invalid address or pagination
/api/attestations:
post:
summary: Create an attestation
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateAttestationRequest"
responses:
"201":
description: Attestation persisted and outbox event emitted
content:
application/json:
schema:
$ref: "#/components/schemas/Attestation"
"400":
description: Invalid or oversized payload
"409":
description: Duplicate attestation
components:
schemas:
Attestation:
type: object
required:
- id
- bondId
- attesterAddress
- subjectAddress
- score
- createdAt
properties:
id:
type: integer
bondId:
type: integer
attesterAddress:
type: string
subjectAddress:
type: string
score:
type: integer
minimum: 0
maximum: 100
note:
type: string
nullable: true
createdAt:
type: string
format: date-time
CreateAttestationRequest:
type: object
required:
- bondId
- attesterAddress
- subject
- value
properties:
bondId:
type: integer
minimum: 1
attesterAddress:
type: string
subject:
type: string
key:
type: string
minLength: 1
maxLength: 128
value:
type: string
minLength: 1
maxLength: 2048
score:
type: integer
minimum: 0
maximum: 100
default: 100
addressSchema:
def:
type: string
Expand Down
42 changes: 2 additions & 40 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,11 @@ import { AnalyticsService } from './services/analytics/service.js'
import { BondService, BondStore } from './services/bond/index.js'
import { createBondRouter } from './routes/bond.js'
import { pool } from './db/pool.js'
import { validate } from './middleware/validate.js'
import { requestIdMiddleware } from './middleware/requestId.js'
import { errorHandler } from './middleware/errorHandler.js'
import { createRateLimitMiddleware } from './middleware/rateLimit.js'
import { validateConfig } from './config/index.js'
import {
buildPaginationMeta,
parsePaginationParams,
} from './lib/pagination.js'
import {
attestationsPathParamsSchema,
createAttestationBodySchema,
} from './schemas/index.js'
import { createAttestationRouter } from './routes/attestations.js'
import { compressionMiddleware, compressionMetricsMiddleware } from './middleware/compression.js'
import { metricsMiddleware, register } from './middleware/metrics.js'

Expand Down Expand Up @@ -72,37 +64,7 @@ app.use('/api/trust', trustRouter)
const bondService = new BondService(new BondStore())
app.use('/api/bond', createBondRouter(bondService))

app.get(
'/api/attestations/:address',
validate({ params: attestationsPathParamsSchema }),
(req, res, next) => {
const { address } = req.validated!.params! as { address: string }
try {
const { page, limit, offset } = parsePaginationParams(req.query as Record<string, unknown>)
res.json({
address,
attestations: [],
offset,
...buildPaginationMeta(0, page, limit),
})
} catch (error) {
next(error)
}
},
)

app.post(
'/api/attestations',
validate({ body: createAttestationBodySchema }),
(req, res) => {
const body = req.validated!.body! as { subject: string; value: string; key?: string }
res.status(201).json({
subject: body.subject,
value: body.value,
key: body.key ?? null,
})
},
)
app.use('/api/attestations', createAttestationRouter())

app.use('/api/bulk', bulkRouter)

Expand Down
41 changes: 41 additions & 0 deletions src/db/repositories/attestationsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ export interface CreateAttestationInput {
note?: string | null
}

export interface ListAttestationsPageOptions {
offset: number
limit: number
}

export interface AttestationPage {
attestations: Attestation[]
total: number
}

type AttestationRow = {
id: string | number
bond_id: string | number
Expand Down Expand Up @@ -90,6 +100,37 @@ export class AttestationsRepository {
return result.rows.map(mapAttestation)
}

async listBySubjectPage(
subjectAddress: string,
options: ListAttestationsPageOptions
): Promise<AttestationPage> {
const [items, count] = await Promise.all([
this.db.query<AttestationRow>(
`
SELECT id, bond_id, attester_address, subject_address, score, note, created_at
FROM attestations
WHERE subject_address = $1
ORDER BY created_at DESC, id DESC
LIMIT $2 OFFSET $3
`,
[subjectAddress, options.limit, options.offset]
),
this.db.query<{ total: string | number }>(
`
SELECT COUNT(*) AS total
FROM attestations
WHERE subject_address = $1
`,
[subjectAddress]
),
])

return {
attestations: items.rows.map(mapAttestation),
total: Number(count.rows[0]?.total ?? 0),
}
}

async listByBond(bondId: number): Promise<Attestation[]> {
const result = await this.db.query<AttestationRow>(
`
Expand Down
Loading
Loading