Skip to content
Open
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
78 changes: 78 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: CI

on:
push:
branches: ["**"]
pull_request:
branches: ["**"]

jobs:
ci:
name: Typecheck · Test · Coverage
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [20.x]

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "npm"

- name: Install dependencies
run: npm ci

# ── Secrets guard ────────────────────────────────────────────────────
# Fail fast if any file tracked by git contains a raw secret pattern.
# This catches accidental commits of .env files or hardcoded credentials.
- name: Check for secrets in repo
run: |
# Reject any file that looks like a real .env (not .env.example)
if git ls-files | grep -E '^\.env$|^\.env\.' ; then
echo "ERROR: .env file is tracked by git — remove it immediately"
exit 1
fi
# Reject placeholder JWT_SECRET if it somehow ends up in source
if git grep -l 'dev-secret-key-change-in-production' -- '*.ts' '*.js' '*.json' 2>/dev/null; then
echo "ERROR: hardcoded dev secret found in source files"
exit 1
fi
echo "Secrets check passed"

# ── Type safety ──────────────────────────────────────────────────────
- name: Typecheck
run: npm run build

# ── Tests (integration suite — tests/) ───────────────────────────────
- name: Test (tests/ suite)
run: npm test
env:
JWT_SECRET: ci-test-secret-at-least-32-chars-long
NODE_ENV: test

# ── Tests (unit suite — src/) ─────────────────────────────────────────
- name: Test (src/ suite)
run: npm run test:src
env:
JWT_SECRET: ci-test-secret-at-least-32-chars-long
NODE_ENV: test

# ── Coverage (integration suite with threshold enforcement) ───────────
- name: Coverage check (tests/ suite)
run: npm run test:coverage
env:
JWT_SECRET: ci-test-secret-at-least-32-chars-long
NODE_ENV: test

# ── Coverage (unit suite — src/ — ≥95% on changed modules) ───────────
- name: Coverage check (src/ suite)
run: npm run test:coverage:src
env:
JWT_SECRET: ci-test-secret-at-least-32-chars-long
NODE_ENV: test
60 changes: 53 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,23 @@ npm start

- Implemented today:
- API info endpoint
- health endpoint
- health endpoint with Redis dependency status
- in-memory stream CRUD placeholder
- indexer freshness classification for `healthy`, `starting`, `stalled`, and `not_configured`
- health-route reporting for indexer freshness
- **Caching layer for hot reads (Redis)**
- `InMemoryCacheClient` for tests, `NullCacheClient` for graceful degradation, `RedisCacheClient` for production
- `GET /api/streams` and `GET /api/streams/:id` served from cache with `X-Cache: HIT/MISS` header
- Cache invalidated on `POST` (create) and `DELETE` (cancel)
- Newly created streams are pre-populated in cache immediately after write
- **Rate limiting** — sliding-window counter per IP, configurable `max`/`windowSeconds`, `X-RateLimit-*` headers, `429` with `Retry-After` on breach, fail-open when cache is unavailable
- **Idempotency** — `Idempotency-Key` header on `POST /api/streams` replays cached response for duplicate submissions, `Idempotent-Replayed: true` header on replay, fail-open when cache is unavailable
- Explicitly not implemented yet:
- a real indexer worker
- durable checkpoint persistence
- database-backed chain state
- automated restart orchestration
- rate limiting or duplicate-delivery protection
- authentication / JWT enforcement

If the health route reports `indexer.status = "stalled"`, treat that as an operational signal that chain-derived views would be stale if the real indexer were enabled in this service.

Expand Down Expand Up @@ -134,10 +141,39 @@ API runs at [http://localhost:3000](http://localhost:3000).
| Method | Path | Description |
| ------ | ------------------ | -------------------------------------------------------------------------------- |
| GET | `/` | API info |
| GET | `/health` | Health check |
| GET | `/api/streams` | List streams |
| GET | `/api/streams/:id` | Get one stream |
| POST | `/api/streams` | Create stream (body: sender, recipient, depositAmount, ratePerSecond, startTime) |
| GET | `/health` | Health check (includes Redis dependency status) |
| GET | `/api/streams` | List streams (cached, `X-Cache` header) |
| GET | `/api/streams/:id` | Get one stream (cached, `X-Cache` header) |
| POST | `/api/streams` | Create stream — supports `Idempotency-Key` header; rate-limited |
| DELETE | `/api/streams/:id` | Cancel stream; invalidates cache |

### Cache behaviour

| Operation | Cache effect |
| ---------------------- | ------------------------------------------------- |
| GET /api/streams | Read from cache (TTL 60 s); MISS populates cache |
| GET /api/streams/:id | Read from cache (TTL 30 s); MISS populates cache |
| POST /api/streams | Pre-populates stream cache; invalidates list cache |
| DELETE /api/streams/:id| Invalidates stream + list cache |

Response header `X-Cache: HIT` or `X-Cache: MISS` is set on all read endpoints.

### Rate limiting

All `/api/streams` routes are rate-limited at **100 requests per 60-second window per IP**.

| Header | Meaning |
| --------------------- | ---------------------------------------- |
| `X-RateLimit-Limit` | Configured maximum |
| `X-RateLimit-Remaining` | Requests left in current window |
| `X-RateLimit-Reset` | Unix timestamp when window resets |
| `Retry-After` | Seconds to wait (only on 429 responses) |

When Redis is unavailable the limiter fails open — requests are allowed through.

### Idempotency

`POST /api/streams` accepts an optional `Idempotency-Key` header (8–128 printable ASCII characters). If the same key is seen within 24 hours, the original response is replayed with `Idempotent-Replayed: true` header set. When Redis is unavailable the check is skipped and the request proceeds normally.

Contract guarantees for this area:

Expand Down Expand Up @@ -321,14 +357,24 @@ Operators can diagnose load-test runs via:
Optional:

- `PORT` - server port, default `3000`
- `REDIS_URL` - Redis connection URL, default `redis://localhost:6379`
- `REDIS_ENABLED` - enable/disable Redis caching, default `true`
- `LOG_LEVEL` - log verbosity (`debug`/`info`/`warn`/`error`), default `info`

Likely future additions:

- `DATABASE_URL`
- `REDIS_URL`
- `HORIZON_URL`
- `JWT_SECRET`

### Running without Redis

Set `REDIS_ENABLED=false` or simply don't run Redis. The service degrades gracefully:
- Cache reads return null (every request is a cache miss)
- Rate limiting is disabled (fail-open)
- Idempotency checks are skipped (fail-open)
- All functional endpoints continue to work normally

## Related repos

- `fluxora-frontend` - dashboard and recipient UI
Expand Down
Binary file added coverage-output.txt
Binary file not shown.
Loading