You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This document specifies the observable behavior of the Fluxora HTTP API under normal and failure conditions. It serves as the source of truth for client expectations and operator diagnostics.
Last Updated: 2024-01-01 Version: 0.1.0 Status: Operator-Grade Reliability
Trust Boundaries
Public Internet Clients
Access: Read-only (GET /health, GET /api/streams, GET /api/streams/{id}, GET /api/admin/status/read-only)
Restrictions: No authentication required; rate limiting applies
Guarantees: Best-effort; no SLA
Authenticated Partners
Access: Create and manage streams (POST /api/streams, GET /api/streams)
Authentication: Bearer token (JWT)
Guarantees: Idempotency via Idempotency-Key header; duplicate detection
Administrators
Access: Full access including internal endpoints
Authentication: Bearer token with admin scope
Guarantees: All partner guarantees plus internal diagnostics
Admin Feature Flags
Storage: Admin pause flags are persisted to local state storage and reloaded at startup.
Read-Only Visibility: GET /api/admin/status/read-only exposes only pauseFlags without requiring admin credentials.
Protected Mutation: PUT /api/admin/pause still requires admin authentication.
Failure Mode: If pause-flag persistence fails during mutation, the API returns 503 Service Unavailable and leaves in-memory flags unchanged.
Inspect statusCode in log; check RPC provider status
CIRCUIT_OPEN
Circuit breaker is OPEN; call was not attempted
Wait for RPC_CB_RESET_TIMEOUT_MS; check upstream health
CANCELLED
Caller aborted via AbortSignal
Expected; no action required
Structured Log Fields
Every failure emits a warn log with these fields:
{
"event": "rpc_failure",
"operation": "getLatestLedger",
"kind": "TIMEOUT",
"statusCode": null,
"durationMs": 5001,
"error": "getLatestLedger timed out after 5000ms"
}
AbortController Usage
Pass an AbortSignal to cancel a call externally:
constcontroller=newAbortController();setTimeout(()=>controller.abort(),3000);// cancel after 3 stry{constledger=awaitrpcService.getLatestLedger({signal: controller.signal});}catch(err){if(errinstanceofRpcProviderError&&err.kind==='CANCELLED'){// call was cancelled — safe to ignore or retry}}
Circuit Breaker Configuration
Env var
Default
Description
RPC_TIMEOUT_MS
5000
Per-call timeout in ms
RPC_CB_FAILURE_THRESHOLD
5
Failures within window before tripping
RPC_CB_WINDOW_MS
30000
Rolling failure-counting window in ms
RPC_CB_RESET_TIMEOUT_MS
60000
Time OPEN before allowing a probe in ms
Failure Modes and Expected Behavior
Condition
kind
Circuit breaker
Client-visible outcome
RPC unreachable
NETWORK
Counts toward threshold
503 Service Unavailable
RPC slow / hung
TIMEOUT
Counts toward threshold
503 Service Unavailable
RPC 5xx response
PROVIDER
Counts toward threshold
503 Service Unavailable
Breaker OPEN
CIRCUIT_OPEN
Already OPEN
503 Service Unavailable (fast-fail)
Caller cancelled
CANCELLED
Does not count
Request aborted; no response sent
Security Notes
Timeout values are read from environment variables at startup; they are not user-controllable at runtime.
AbortSignal cancellation does not suppress circuit-breaker accounting — only CANCELLED failures are excluded from the failure count.
No RPC credentials or internal error details are forwarded to HTTP clients; only 503 with a generic message is returned.
Worker Queue Full
Trigger: Indexer worker queue exceeds capacity
Status: 503
Code: SERVICE_UNAVAILABLE
Message: "Service temporarily unavailable"
Behavior: POST /internal/indexer/sync returns 503
Recovery: Automatic; retry after 60 seconds
Partial Data
Stale Stream Listing
Trigger: Database lag or Stellar RPC delay
Status: 200
Behavior: Stream list may not include very recent streams
Guarantee: Eventual consistency within 5 minutes
Mitigation: Use cursor-based pagination; check stream status endpoint
Missing Stream Details
Trigger: Stream created but not yet indexed
Status: 404
Behavior: GET /api/streams/{id} returns 404 immediately after creation
Guarantee: Stream will be available within 30 seconds
Mitigation: Retry with exponential backoff
Idempotency Guarantees
Exactly-Once Semantics
Scope: POST /api/streams (stream creation)
Mechanism: Idempotency-Key request header + SHA-256 fingerprint of normalised body
Duration: Process lifetime (in-memory store); Redis-backed store recommended for production (24-hour TTL)
Guarantee: Same Idempotency-Key + same body = same response, served from cache
Idempotency-Key Format
Required: Yes — missing or malformed key returns 400 VALIDATION_ERROR
If the database upsert fails (e.g. pool exhausted → 503), the idempotency key is not stored. The client may safely retry with the same key and body once the dependency recovers.
Error Response Format
All error responses follow this standardized structure:
When the Stellar RPC provider becomes unreachable the backend activates a degradation policy enforced by the rpcDegradation middleware. The policy is observable, deterministic, and documented here so that clients and operators can reason about behavior during an outage without guessing.
One probe call is allowed to test recovery — treated as degraded until the probe succeeds
The breaker trips when failureThreshold failures occur within the rolling windowMs window. It stays OPEN for resetTimeoutMs before transitioning to HALF_OPEN.
Client-Visible Outcomes
Condition
HTTP Method
Status
Response Headers
Body
Circuit CLOSED
Any
Normal route response
X-Degradation-State: CLOSED
Normal response body
Circuit OPEN / HALF_OPEN
GET, HEAD, OPTIONS
200 (stale data)
Warning: 199 fluxora-backend "Stellar RPC unavailable - data may be stale", X-Degradation-State: OPEN
Check X-Degradation-State header on any response or query /health
If OPEN: inspect structured logs for rpc_failure events to identify the RPC provider issue
If sustained: consider manual resetCircuit() after verifying RPC provider recovery
If resolved: confirm X-Degradation-State: CLOSED on subsequent requests
Decimal String Serialization Guarantee
The degradation middleware does not modify response bodies. All amount fields (depositAmount, ratePerSecond, etc.) continue to be serialized as decimal strings per the project-wide serialization policy, regardless of degradation state.
Manual check: trip the circuit via repeated RPC failures and verify:
GET /api/streams returns 200 with Warning and X-Degradation-State: OPEN
POST /api/streams returns 503 with degradation diagnostics
CORS Policy
Overview
Cross-Origin Resource Sharing (CORS) is enforced by corsAllowlistMiddleware in src/middleware/cors.ts, applied globally before all routes. The policy differs between development and production environments.
Environment Behaviour
Environment
Allowed origins
Preflight result
Non-production (NODE_ENV !== 'production')
Any origin
204 No Content with full CORS headers
Production
Origins listed in CORS_ALLOWED_ORIGINS
204 No Content if allowed; 403 if denied
Configuration
Set CORS_ALLOWED_ORIGINS as a comma-separated list of exact origin strings:
Whitespace around each entry is trimmed automatically.
An empty or unset value means no origin is allowed in production.
Response Headers
Header
When present
Value
Access-Control-Allow-Origin
Origin is allowed
Echoed request Origin value
Vary
Origin is allowed
Origin
Access-Control-Allow-Methods
Origin is allowed
GET,POST,PUT,PATCH,DELETE,OPTIONS
Access-Control-Allow-Headers
Origin is allowed
Echoed Access-Control-Request-Headers if present; otherwise Content-Type,Authorization,X-Correlation-ID
Access-Control-Max-Age
Preflight only
86400 (24 hours)
Preflight Handling
A preflight request is an OPTIONS request that carries an Origin header.
Allowed origin → 204 No Content with all CORS headers including Access-Control-Max-Age: 86400.
Denied origin → 403 Forbidden with body { "error": { "code": "CORS_ORIGIN_DENIED", "message": "Origin is not allowed by CORS policy" } }.
No Origin header → 204 No Content with no CORS headers (non-browser probe; passes through).
Non-Preflight Requests
Allowed origin → CORS headers are set; request continues to the route handler.
Denied origin → No CORS headers; request continues to the route handler (browser will block the response client-side).
No Origin header → Request continues to the route handler unchanged.
Failure Modes
Condition
Expected behaviour
CORS_ALLOWED_ORIGINS unset in production
All origins denied; preflight returns 403
Origin not in allowlist (preflight)
403 with CORS_ORIGIN_DENIED
Origin not in allowlist (non-preflight)
No CORS headers; browser enforces same-origin policy
OPTIONS without Origin
204 — treated as a non-browser probe
Security Notes
Origins are matched exactly (no wildcard or prefix matching in production).
The Vary: Origin header is always set when an origin is allowed, preventing CDN caching of origin-specific responses.
Access-Control-Allow-Headers echoes the client's Access-Control-Request-Headers to avoid blocking legitimate custom headers while still requiring the browser to declare them.
Access-Control-Max-Age: 86400 reduces preflight round-trips without weakening security.
Verification Evidence
Automated tests: tests/cors.test.ts (16 cases, ≥95% coverage of src/middleware/cors.ts)
Content Security Policy
Overview
The CSP is enforced by createHelmetMiddleware in src/middleware/helmet.ts, applied globally before all routes. A per-request nonce is generated by cspNonceMiddleware (mounted immediately before helmet) and embedded in the Content-Security-Policy header on every response.
Policy Directives
Directive
Value
Notes
default-src
'self'
Baseline fallback
script-src
'self' 'nonce-<request-nonce>'
No 'unsafe-inline' or 'unsafe-eval'
style-src
'self' 'nonce-<request-nonce>'
No 'unsafe-inline' — deviation from baseline
img-src
'self' data: https:
Allows data URIs and HTTPS images
connect-src
'self'
XHR/fetch restricted to same origin
font-src
'self'
object-src
'none'
Blocks plugins (Flash, etc.)
media-src
'self'
frame-src
'none'
Prevents framing by any origin
upgrade-insecure-requests
(present)
Instructs browsers to upgrade HTTP sub-resources to HTTPS
Nonce Mechanism
A 16-byte cryptographically random nonce is generated per request via crypto.randomBytes(16).toString('base64').
The nonce is stored in res.locals.cspNonce and injected into script-src and style-src as 'nonce-<value>'.
Each response carries a unique nonce; replay of a captured nonce in a different response is rejected by the browser.
Deviations from Baseline
Deviation
Rationale
'unsafe-inline' removed from style-src
Eliminates a common XSS vector. Inline styles must use the per-request nonce instead.
upgrade-insecure-requests added
Instructs browsers to upgrade mixed-content sub-resources without requiring a separate HSTS preload entry.
frame-src 'none' (explicit)
Belt-and-suspenders alongside X-Frame-Options: SAMEORIGIN; frame-src takes precedence in CSP-aware browsers.
Trust Boundaries
Actor
May do
May not do
Public internet clients
Observe the Content-Security-Policy header and use the nonce for inline scripts/styles
Reuse a nonce from a previous response
Authenticated partners
Same as public clients
Negotiate a weaker CSP policy
Administrators / operators
Verify CSP presence via curl -I or smoke tests
Disable CSP at runtime without a deploy
Failure Modes
Condition
Expected behavior
Route error (4xx/5xx)
CSP header is still emitted; nonce is still unique
Dependency outage
CSP header is still emitted regardless of upstream state
Operator Observability
Smoke check: curl -I http://127.0.0.1:3000/health — confirm content-security-policy is present and contains nonce-.
Two consecutive requests should produce different nonce values.
Verification Evidence
Automated tests: tests/helmet.test.ts — covers baseline headers plus strict CSP assertions (no unsafe-inline, unique nonce per request, object-src/frame-src none, upgrade-insecure-requests).
Non-Goals & Deferred Work
Out of Scope (v0.1.0)
WebSocket subscriptions for real-time updates
Batch stream creation endpoint
Stream cancellation endpoint
Advanced filtering (by amount, date range, etc.)
Webhook notifications
GraphQL API
Follow-Up Issues
#8: WebSocket subscriptions for stream updates
#9: Batch stream creation for bulk operations
#10: Stream cancellation and refund logic
#11: Advanced filtering and search
#12: Webhook notifications for stream events
Testing & Verification
Unit Tests
Coverage: ≥95% for validation, helpers, error handling