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
3 changes: 2 additions & 1 deletion API_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ Quick reference for the most common codes:

The API implements rate limiting to ensure fair usage:

- **Rate Limit:** 1000 requests per minute per API key
- **Global limit:** 100 requests per 60 seconds per client IP (`GLOBAL_RATE_LIMIT_MAX`, `GLOBAL_RATE_LIMIT_WINDOW_SECS`)
- **Admin routes:** 30 requests per 60 seconds (`ADMIN_RATE_LIMIT_MAX`, `ADMIN_RATE_LIMIT_WINDOW_SECS`)
- **Headers:**
- `X-RateLimit-Limit`: Maximum requests per window
- `X-RateLimit-Remaining`: Requests remaining in current window
Expand Down
1 change: 1 addition & 0 deletions docs/API_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
|--------|------|-----------|------|
| POST | `/api/v1/markets/{market_id}/resolve` | `resolveMarket` | ApiKeyAuth |
| POST | `/api/v1/markets/{market_id}/cancel` | `cancelMarket` | ApiKeyAuth |
| POST | `/api/v1/blockchain/replay` | `blockchainReplay` | ApiKeyAuth |
| POST | `/api/blockchain/replay` | `blockchainReplay` | ApiKeyAuth |
| GET | `/api/v1/email/preview/{template_name}` | `emailPreview` | ApiKeyAuth |
| POST | `/api/v1/email/test` | `emailSendTest` | ApiKeyAuth |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ describe('API client paths match schema.d.ts (contract test, #3)', () => {
'/api/v1/email/test',
'/api/v1/email/analytics',
'/api/v1/email/queue/stats',
'/api/blockchain/replay',
'/api/v1/blockchain/replay',
]);

const PLACEHOLDER = new RegExp(String.raw`\$\{[^}]+\}`, 'g');
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/api/rateLimit.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Rate-limit signal bus (#1339).
*
* `API_SPEC.md`: 1000 req/min per key; a 429 carries `Retry-After` (seconds). The
* `API_SPEC.md`: 100 req/60s per IP (30/min on admin routes); a 429 carries `Retry-After` (seconds). The
* client reports every 429 here; the UI subscribes to show one shared countdown.
*
* Coalescing: while a cooldown window is active, further 429s from parallel requests
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/api/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/blockchain/replay": {
"/api/v1/blockchain/replay": {
parameters: {
query?: never;
header?: never;
Expand Down
10 changes: 9 additions & 1 deletion services/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -783,7 +783,7 @@ paths:
"500":
$ref: "#/components/responses/ApiError"

/api/blockchain/replay:
/api/v1/blockchain/replay:
post:
tags: [blockchain]
operationId: blockchainReplay
Expand Down Expand Up @@ -815,6 +815,14 @@ paths:
summary: List dead-letter email jobs (admin)
security:
- ApiKeyAuth: []
parameters:
- name: limit
in: query
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
- name: cursor
in: query
description: Opaque cursor from a previous response's `next_cursor`
schema: { type: string }
responses:
"200":
description: Dead-letter job list
Expand Down
18 changes: 10 additions & 8 deletions services/api/scripts/run_migrations.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
# How it works:
# 1. Ensures the schema_migrations tracking table exists (000_ migration).
# 2. For each *.sql file in migrations/ (sorted by filename):
# a. Derives the version from the numeric prefix (e.g. "001").
# a. Uses the full filename stem as the version (e.g. "017_create_api_keys"),
# so files sharing a numeric prefix are all applied.
# b. Skips the file if that version is already recorded.
# c. Computes a SHA-256 checksum of the file.
# d. Runs the SQL inside a transaction.
Expand Down Expand Up @@ -74,8 +75,9 @@ ensure_tracking_table() {
is_applied() {
local version="$1"
local count
# Legacy rows stored only the numeric prefix; honour them when the recorded name matches.
count=$(psql_exec -t -c \
"SELECT COUNT(*) FROM schema_migrations WHERE version = '$version';" \
"SELECT COUNT(*) FROM schema_migrations WHERE version = '$version' OR (version = '${version%%_*}' AND name = '$version');" \
2>/dev/null | tr -d '[:space:]')
[[ "$count" == "1" ]]
}
Expand Down Expand Up @@ -119,19 +121,19 @@ cmd_status() {
for file in $(ls "$MIGRATIONS_DIR"/*.sql 2>/dev/null | sort); do
local fname version name
fname="$(basename "$file")"
version="${fname%%_*}"
name="${fname%.sql}"
version="$name"

if is_applied "$version"; then
local row
row=$(psql_exec -t -c \
"SELECT applied_at, checksum FROM schema_migrations WHERE version = '$version';" \
"SELECT applied_at, checksum FROM schema_migrations WHERE version = '$version' OR (version = '${version%%_*}' AND name = '$version');" \
| tr -d '[:space:]')
local applied_at="${row%%|*}"
local chk="${row##*|}"
printf " %-8s %-50s %-26s %s\n" "$version" "$name" "$applied_at" "${chk:0:12}..."
printf " %-8s %-50s %-26s %s\n" "${version%%_*}" "$name" "$applied_at" "${chk:0:12}..."
else
printf " %-8s %-50s %-26s %s\n" "$version" "$name" "(pending)" "-"
printf " %-8s %-50s %-26s %s\n" "${version%%_*}" "$name" "(pending)" "-"
any_pending=true
fi
done
Expand All @@ -149,7 +151,7 @@ cmd_status() {
# ---------------------------------------------------------------------------
cmd_rollback() {
local target_version="$1"
[[ -n "$target_version" ]] || die "--rollback requires a version argument (e.g. --rollback 005)"
[[ -n "$target_version" ]] || die "--rollback requires a version argument (e.g. --rollback 005_create_content_management)"

ensure_tracking_table

Expand Down Expand Up @@ -205,7 +207,7 @@ cmd_run() {
for file in $files; do
local fname version
fname="$(basename "$file")"
version="${fname%%_*}"
version="${fname%.sql}"

if is_applied "$version"; then
log "Skipping [$version] — already applied"
Expand Down
6 changes: 4 additions & 2 deletions services/api/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -973,11 +973,13 @@ impl Database {
/// List all dead-letter job IDs from PostgreSQL (oldest-failed first).
///
/// Used as a fallback when Redis is unavailable or has been flushed.
pub async fn email_list_dead_letter_jobs(&self) -> anyhow::Result<Vec<uuid::Uuid>> {
pub async fn email_list_dead_letter_jobs(&self, limit: i64, offset: i64) -> anyhow::Result<Vec<uuid::Uuid>> {
let rows = self
.with_timeout(
"email_list_dead_letter_jobs",
sqlx::query("SELECT id FROM email_dead_letter_jobs ORDER BY failed_at ASC")
sqlx::query("SELECT id FROM email_dead_letter_jobs ORDER BY failed_at ASC LIMIT $1 OFFSET $2")
.bind(limit)
.bind(offset)
.fetch_all(&self.pool),
)
.await
Expand Down
4 changes: 2 additions & 2 deletions services/api/src/email/queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,10 +341,10 @@ impl EmailQueue {
}

/// List all job IDs currently in the dead-letter set (oldest-failed first).
pub async fn list_dead_letter(&self) -> Result<Vec<Uuid>> {
pub async fn list_dead_letter(&self, offset: isize, limit: isize) -> Result<Vec<Uuid>> {
let mut conn = self.cache.get_connection().await?;
let items: Vec<String> = conn
.zrange(EMAIL_DEAD_LETTER_KEY, 0isize, -1isize)
.zrange(EMAIL_DEAD_LETTER_KEY, offset, offset + limit - 1)
.await
.context("Failed to list dead-letter set")?;

Expand Down
7 changes: 6 additions & 1 deletion services/api/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -443,9 +443,14 @@ async fn main() -> anyhow::Result<()> {
post(handlers::cancel_market),
)
.route(
"/api/blockchain/replay",
"/api/v1/blockchain/replay",
post(handlers::blockchain_replay),
)
// Deprecated unversioned alias; use /api/v1/blockchain/replay.
.route(
"/api/blockchain/replay",
post(handlers::blockchain_replay_deprecated),
)
.route(
"/api/v1/email/preview/:template_name",
get(handlers::email_preview),
Expand Down
4 changes: 2 additions & 2 deletions services/api/tests/openapi_contract_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ mod tests {
("POST", "/api/v1/email/test"),
("GET", "/api/v1/email/analytics"),
("GET", "/api/v1/email/queue/stats"),
("POST", "/api/blockchain/replay"),
("POST", "/api/v1/blockchain/replay"),
("GET", "/api/v1/email/queue/dead-letter"),
("POST", "/api/v1/email/queue/dead-letter/{job_id}/requeue"),
("GET", "/api/v1/audit/logs"),
Expand All @@ -50,7 +50,7 @@ mod tests {
("POST", "/api/v1/email/test"),
("GET", "/api/v1/email/analytics"),
("GET", "/api/v1/email/queue/stats"),
("POST", "/api/blockchain/replay"),
("POST", "/api/v1/blockchain/replay"),
("GET", "/api/v1/email/queue/dead-letter"),
("POST", "/api/v1/email/queue/dead-letter/{job_id}/requeue"),
("GET", "/api/v1/audit/logs"),
Expand Down