feat(api): error codes, idempotency keys, cursor pagination

- Stable machine-readable error codes alongside the error message
  (UNAUTHENTICATED, INVALID_TOKEN, ADMIN_REQUIRED, SCOPE_INSUFFICIENT,
  IDEMPOTENCY_IN_PROGRESS); CLI renders "CODE: message".
- Idempotency middleware on authed/admin groups: POST requests with an
  Idempotency-Key header replay the first response for 24h (Redis-backed,
  caller-scoped); concurrent duplicates get 409. X-Idempotent-Replay
  header marks replays.
- Shared pagination helpers: ?cursor= (opaque, preferred) alongside
  legacy ?page= on projects and deployments lists; pagination meta
  includes next_cursor; invalid cursors fail soft to page 1.
- CLI --idempotency-key flag for safe scripted retries.
- OpenAPI updated: error envelope, cursor params, idempotency contract.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-10-08 15:38:36 +02:00
1 parent 485e41a817
commit 18f78f00a3
15 files changed
+454 -56

No files matched your search

+29
View File
@@ -0,0 +1,29 @@
package api
import (
"github.com/gin-gonic/gin"
)
// Stable machine-readable error codes for agent clients. The "error" field
// stays human-readable; "code" is the contract the CLI/MCP rely on.
const (
CodeUnauthorized = "UNAUTHENTICATED"
CodeInvalidToken = "INVALID_TOKEN"
CodeForbidden = "FORBIDDEN"
CodeAdminRequired = "ADMIN_REQUIRED"
CodeScopeInsufficient = "SCOPE_INSUFFICIENT"
CodeNotFound = "NOT_FOUND"
CodeValidation = "VALIDATION"
CodeConflict = "CONFLICT"
CodeInternal = "INTERNAL"
CodeUnavailable = "DEPENDENCY_UNAVAILABLE"
CodeIdempotencyRunning = "IDEMPOTENCY_IN_PROGRESS"
)
// respondError emits the standard error envelope. Existing handlers return
// bare {"error": msg} — this adds the stable code for new and
// agent-facing paths.
func respondError(c *gin.Context, status int, code, message string) {
c.JSON(status, gin.H{"error": message, "code": code})
c.Abort()
}