Initial Commit
This commit is contained in:
192
openspec/changes/enterprise-api-gateway/proposal.md
Normal file
192
openspec/changes/enterprise-api-gateway/proposal.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# OpenSpec Change: enterprise-api-gateway
|
||||
|
||||
- **Change ID**: enterprise-api-gateway
|
||||
- **Package**: `@suaveplan/api-gateway`
|
||||
- **Category**: api/framework
|
||||
- **Target path**: `api/framework/api-gateway`
|
||||
- **Status**: PROPOSED
|
||||
- **Date**: 2026-04-24
|
||||
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/api-gateway.md`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Deliver `@suaveplan/api-gateway` on the genesis branch from scratch at enterprise quality. The package provides a transport-agnostic API gateway layer: pattern-based `Router`, pluggable-strategy `LoadBalancer` (round-robin / random / weighted / consistent-hash), `TransformerPipeline` (request/response rewrite helpers), `GatewayCircuitBreaker` (closed → open → half-open state machine), active `HealthChecker`, per-route retry policy via `@suaveplan/retry`, rate-limiter edge hook via `@suaveplan/rate-limiter`, and a Hono adapter. All wired under `Gateway.handle(request)`. Genesis adds structured errors, Zod config schemas, OTel instrumentation, `@suaveplan/logger` structured logging, property-based tests, and a benchmark suite.
|
||||
|
||||
---
|
||||
|
||||
## Current-state audit (genesis)
|
||||
|
||||
The package does not exist on genesis yet — fresh build. Reference: `/mnt/s/web/suaveplan/enterprise/api-gateway.md`.
|
||||
|
||||
---
|
||||
|
||||
## Available workspace deps on genesis
|
||||
|
||||
| Package | Role |
|
||||
|---|---|
|
||||
| `@suaveplan/error` (`workspace:^`) | `GatewayError` hierarchy |
|
||||
| `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock` |
|
||||
| `@suaveplan/logger` (`workspace:^`) | Per-request structured logging |
|
||||
| `@suaveplan/retry` (`workspace:^`) | Per-route retry + backoff |
|
||||
| `@suaveplan/rate-limiter` (`workspace:^`) | Gateway-edge rate limiting |
|
||||
| `@suaveplan/events` (`workspace:^`) | Circuit-breaker state-change events |
|
||||
| `@suaveplan/api` (`workspace:^`) | Hono adapter mount |
|
||||
| `hono` (`catalog:server`) | HTTP types |
|
||||
| `zod` (`catalog:forms`) | Schema validation |
|
||||
| `@opentelemetry/api` (`catalog:observability`) | Spans + metrics |
|
||||
| `tslib` (`catalog:build`) | Runtime helpers |
|
||||
|
||||
---
|
||||
|
||||
## Public API surface
|
||||
|
||||
### Core classes and factories
|
||||
|
||||
```ts
|
||||
function createGateway(config: GatewayConfig): Gateway
|
||||
class Gateway { handle(request: GatewayRequest): Promise<GatewayResult> }
|
||||
class Router { match(req: GatewayRequest): RouteMatch | null }
|
||||
class LoadBalancer { select(targets: Target[], strategy: BalancerStrategy): Target }
|
||||
class TransformerPipeline { run(req: GatewayRequest): GatewayRequest; runResponse(res: GatewayResponse): GatewayResponse }
|
||||
class GatewayCircuitBreaker { state: CircuitState; record(success: boolean): void }
|
||||
```
|
||||
|
||||
### Transformer helpers
|
||||
|
||||
```ts
|
||||
function addHeaders(headers: Record<string, string>): RequestTransformer
|
||||
function addPathPrefix(prefix: string): RequestTransformer
|
||||
function addResponseHeaders(headers: Record<string, string>): ResponseTransformer
|
||||
function removeHeaders(names: string[]): RequestTransformer
|
||||
function rewritePath(pattern: RegExp, replacement: string): RequestTransformer
|
||||
function stripPathPrefix(prefix: string): RequestTransformer
|
||||
```
|
||||
|
||||
### Interfaces
|
||||
|
||||
```ts
|
||||
interface IGateway { handle(request: GatewayRequest): Promise<GatewayResult> }
|
||||
interface IRouter { match(req: GatewayRequest): RouteMatch | null }
|
||||
interface ILoadBalancer { select(targets: Target[], strategy: BalancerStrategy): Target }
|
||||
interface ITransformerPipeline { run(req: GatewayRequest): GatewayRequest }
|
||||
```
|
||||
|
||||
### Key types
|
||||
|
||||
```ts
|
||||
type GatewayConfig, GatewayRequest, GatewayResponse, GatewayResult
|
||||
type RouteDefinition, RouteMatch, Target, TargetHealth
|
||||
type CircuitBreakerConfig, CircuitBreakerState, CircuitState
|
||||
type BalancerStrategy, HttpMethod, RequestTransformer, ResponseTransformer
|
||||
```
|
||||
|
||||
### Error codes
|
||||
|
||||
```ts
|
||||
type GatewayErrorCode =
|
||||
| "GATEWAY_NO_ROUTE"
|
||||
| "GATEWAY_CIRCUIT_OPEN"
|
||||
| "GATEWAY_UPSTREAM_TIMEOUT"
|
||||
| "GATEWAY_UPSTREAM_UNREACHABLE"
|
||||
| "GATEWAY_CONFIG_INVALID"
|
||||
| "GATEWAY_TRANSFORM_FAILED"
|
||||
```
|
||||
|
||||
### Constants
|
||||
|
||||
```ts
|
||||
DEFAULT_BALANCER_STRATEGY, DEFAULT_FAILURE_THRESHOLD, DEFAULT_RESET_TIMEOUT
|
||||
DEFAULT_SUCCESS_THRESHOLD, DEFAULT_TIMEOUT, GATEWAY_STATUS, MAX_ROUTES
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Genesis additions
|
||||
|
||||
1. **`fetcher` port** — `GatewayConfig.fetcher: (req: GatewayRequest) => Promise<GatewayResponse>` — explicit outbound HTTP call; injectable for testing.
|
||||
2. **Zod schemas** for `GatewayConfig`, `RouteDefinition`, `CircuitBreakerConfig` with `createGateway` parse guard.
|
||||
3. **Structured `GatewayError`** family extending `SuaveplanError`.
|
||||
4. **OTel** — span per `handle()` call with `gateway.route`, `gateway.target`, `gateway.attempt`, `gateway.circuit_state` attributes; `gateway.requests_total` counter; `gateway.upstream_duration_ms` histogram.
|
||||
5. **`@suaveplan/retry`** integration on `RouteDefinition.retry` — `attempts`, `backoff`, `jitter`, `retryableStatuses`.
|
||||
6. **`HealthChecker`** interface + default interval-based probe that updates `Target.health`.
|
||||
7. **`@suaveplan/rate-limiter`** hook in `Gateway.handle()` checked before forwarding.
|
||||
8. **Hono adapter** — `gatewayMiddleware(gateway: IGateway): MiddlewareHandler`.
|
||||
9. **Property-based tests** with `fast-check` on router, balancer, and circuit-breaker invariants.
|
||||
10. **Benchmark suite** measuring `handle()` latency.
|
||||
|
||||
---
|
||||
|
||||
## File layout
|
||||
|
||||
```text
|
||||
api/framework/api-gateway/
|
||||
package.json
|
||||
tsconfig.json
|
||||
vitest.config.ts
|
||||
src/
|
||||
index.ts
|
||||
constants/index.ts
|
||||
types/
|
||||
common.ts
|
||||
common.md
|
||||
errors/
|
||||
gateway-errors.ts
|
||||
gateway-errors.md
|
||||
gateway-errors.test.ts
|
||||
index.ts
|
||||
schemas/
|
||||
config.schema.ts
|
||||
route.schema.ts
|
||||
circuit-breaker.schema.ts
|
||||
core/
|
||||
gateway/
|
||||
gateway.ts
|
||||
gateway.md
|
||||
gateway.types.ts
|
||||
gateway.test.ts
|
||||
index.ts
|
||||
router/
|
||||
router.ts
|
||||
router.md
|
||||
router.test.ts
|
||||
index.ts
|
||||
balancer/
|
||||
balancer.ts
|
||||
balancer.md
|
||||
balancer.test.ts
|
||||
index.ts
|
||||
transformer/
|
||||
transformer.ts
|
||||
transformer.md
|
||||
transformer.test.ts
|
||||
index.ts
|
||||
circuit-breaker/
|
||||
circuit-breaker.ts
|
||||
circuit-breaker.md
|
||||
circuit-breaker.test.ts
|
||||
index.ts
|
||||
health/
|
||||
health.ts
|
||||
health.md
|
||||
health.test.ts
|
||||
index.ts
|
||||
adapters/
|
||||
hono-adapter/
|
||||
hono-adapter.ts
|
||||
hono-adapter.md
|
||||
hono-adapter.test.ts
|
||||
index.ts
|
||||
__benchmarks__/
|
||||
gateway.bench.ts
|
||||
tests/
|
||||
integration.test.ts
|
||||
property.test.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Impact
|
||||
|
||||
**Downstream**: any server application that needs to proxy or compose multiple backends. **Prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/logger`, `@suaveplan/retry`, `@suaveplan/rate-limiter`, `@suaveplan/events`, `@suaveplan/api`.
|
||||
139
openspec/changes/enterprise-api-gateway/tasks.md
Normal file
139
openspec/changes/enterprise-api-gateway/tasks.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Tasks: enterprise-api-gateway
|
||||
|
||||
**Change ID**: enterprise-api-gateway
|
||||
**Package**: `@suaveplan/api-gateway`
|
||||
**Target path**: `api/framework/api-gateway`
|
||||
**Prerequisite packages on Verdaccio**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/logger`, `@suaveplan/retry`, `@suaveplan/rate-limiter`, `@suaveplan/events`, `@suaveplan/api`
|
||||
|
||||
---
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
|
||||
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
|
||||
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
|
||||
|
||||
## 1. Scaffold
|
||||
|
||||
- [ ] Create `api/framework/api-gateway/` with all subdirs per layout.
|
||||
- [ ] `package.json` v0.0.0, `"sideEffects": false`, subpath exports per feature module.
|
||||
- [ ] `tsconfig.json` extending `library-node.json`.
|
||||
- [ ] `vitest.config.ts` via `createTestConfig()`.
|
||||
- [ ] Scripts: `lint`, `typecheck`, `test`, `test:coverage`, `build`, `docs:check`, `bench`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Types, schemas, errors
|
||||
|
||||
- [ ] `src/types/common.ts` — all interfaces and type aliases listed in proposal.
|
||||
- [ ] `src/types/common.md` (≥200 words, API reference, both examples).
|
||||
- [ ] `src/schemas/config.schema.ts`, `route.schema.ts`, `circuit-breaker.schema.ts` — Zod schemas; `createGateway` parses with `GatewayConfigSchema.parse(config)`.
|
||||
- [ ] `src/errors/gateway-errors.ts` — 6-code union, metadata, `registerGatewayErrorCodes()`, `GatewayError` re-export.
|
||||
- [ ] `gateway-errors.test.ts` and `gateway-errors.md`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Router
|
||||
|
||||
- [ ] `Router`: path-pattern matching (static + `:param` + wildcard `*`), method matching, ordered by specificity (static > param > wildcard).
|
||||
- [ ] `match(req)` returns first match or `null`.
|
||||
- [ ] `router.test.ts`: static match, param extraction, wildcard, method mismatch returns null, method `*` matches any.
|
||||
- [ ] `router.md`.
|
||||
|
||||
---
|
||||
|
||||
## 4. LoadBalancer
|
||||
|
||||
- [ ] Strategies: `round-robin` (stateful counter), `random`, `weighted` (weight-proportional selection), `consistent-hash` (FNV-1a on a configurable key).
|
||||
- [ ] Skip targets with `health === 'unhealthy'`.
|
||||
- [ ] `balancer.test.ts`: each strategy, all-unhealthy throws `GatewayError(GATEWAY_NO_ROUTE)`.
|
||||
- [ ] `balancer.md`.
|
||||
|
||||
---
|
||||
|
||||
## 5. TransformerPipeline + helpers
|
||||
|
||||
- [ ] `TransformerPipeline`: ordered list of `RequestTransformer | ResponseTransformer`; `run` applies request transformers, `runResponse` applies response transformers; catch on transformer throws `GatewayError(GATEWAY_TRANSFORM_FAILED)`.
|
||||
- [ ] Implement all six helper factories listed in proposal.
|
||||
- [ ] `transformer.test.ts`: each helper, pipeline ordering, error wrapping.
|
||||
- [ ] `transformer.md`.
|
||||
|
||||
---
|
||||
|
||||
## 6. GatewayCircuitBreaker
|
||||
|
||||
- [ ] States: `closed` → `open` (after `failureThreshold` consecutive failures) → `half-open` (after `resetTimeout` ms) → `closed` (after `successThreshold` successes in half-open).
|
||||
- [ ] `record(success)` transitions state; emits `gateway:circuit:state` event on `@suaveplan/events` bus when state changes.
|
||||
- [ ] Injectable `Clock` for timeout tracking.
|
||||
- [ ] `circuit-breaker.test.ts`: full state machine cycle; half-open success restores, failure re-opens; open throws `GatewayError(GATEWAY_CIRCUIT_OPEN)`.
|
||||
- [ ] `circuit-breaker.md`.
|
||||
|
||||
---
|
||||
|
||||
## 7. HealthChecker
|
||||
|
||||
- [ ] `HealthChecker` interface: `start()`, `stop()`, `getHealth(targetId): TargetHealth`.
|
||||
- [ ] Default interval-based implementation: probe via configurable `probe: (target: Target) => Promise<boolean>`; update `target.health`; emit `gateway:target:health` event on state change.
|
||||
- [ ] `health.test.ts`: probe called on interval, healthy → unhealthy transition emits event, `stop()` clears interval.
|
||||
- [ ] `health.md`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Gateway
|
||||
|
||||
- [ ] `createGateway(config)`: parse config via Zod, construct router + balancer + circuit-breaker + transformer pipeline.
|
||||
- [ ] `handle(request)`:
|
||||
1. Rate-limit check (if limiter injected) → 429 via `GatewayError`.
|
||||
2. `Router.match` → null throws `GatewayError(GATEWAY_NO_ROUTE)`.
|
||||
3. Circuit-breaker open → throws `GatewayError(GATEWAY_CIRCUIT_OPEN)`.
|
||||
4. Load-balance to target.
|
||||
5. Apply request transformers.
|
||||
6. Call `config.fetcher` with retry policy (`@suaveplan/retry`).
|
||||
7. Apply response transformers.
|
||||
8. Record circuit-breaker outcome.
|
||||
9. Return `GatewayResult`.
|
||||
- [ ] Wrap entire flow in OTel span `gateway.handle`.
|
||||
- [ ] `gateway.test.ts` (≤500 lines): happy path, no-route, circuit open, upstream timeout, retry succeeds on second attempt, transformer rewrite applied.
|
||||
- [ ] `gateway.md`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Hono adapter
|
||||
|
||||
- [ ] `gatewayMiddleware(gateway: IGateway): MiddlewareHandler` — converts Hono `Request` → `GatewayRequest`, calls `gateway.handle`, maps `GatewayResult` → Hono `Response`.
|
||||
- [ ] `hono-adapter.test.ts`: 200 forwarded, 404 no-route, circuit-open → 503.
|
||||
- [ ] `hono-adapter.md`.
|
||||
|
||||
---
|
||||
|
||||
## 10. Property-based tests (`tests/property.test.ts`)
|
||||
|
||||
- [ ] **Router invariant**: for any route table + request, `match` returns either a consistent route or null (never throws); matched route's method contains the request method.
|
||||
- [ ] **Balancer fairness**: round-robin distributes N requests across K healthy targets with equal ±1 frequency.
|
||||
- [ ] **Circuit-breaker monotonicity**: for any sequence of boolean outcomes, the state sequence never transitions `open → closed` directly (always through `half-open`).
|
||||
- [ ] Timeout 30 000 ms on each `it()`.
|
||||
|
||||
---
|
||||
|
||||
## 11. Benchmarks (`src/__benchmarks__/gateway.bench.ts`)
|
||||
|
||||
- [ ] `gateway_handle_passthrough`: `handle()` with 1 target, no transforms, in-process no-op fetcher.
|
||||
- [ ] `gateway_handle_5_transforms`: same but with 5 transformers.
|
||||
|
||||
---
|
||||
|
||||
## 12. Integration tests (`tests/integration.test.ts`)
|
||||
|
||||
- [ ] Wire a real Hono upstream server; `Gateway.handle` proxies to it; assert response forwarded correctly.
|
||||
- [ ] Circuit-breaker trips after threshold; half-open probe restores.
|
||||
|
||||
---
|
||||
|
||||
## 13. Docs and publish
|
||||
|
||||
- [ ] All co-located `.md` files (≥200 words, both examples, full API reference).
|
||||
- [ ] Root `README.md`.
|
||||
- [ ] `bunx turbo lint typecheck test --filter=@suaveplan/api-gateway` — green.
|
||||
- [ ] `bunx turbo test:coverage --filter=@suaveplan/api-gateway` — 100%.
|
||||
- [ ] `bun scripts/publish.ts --package @suaveplan/api-gateway`.
|
||||
- [ ] Archive → `openspec/archive/2026-04-24-enterprise-api-gateway/`.
|
||||
Reference in New Issue
Block a user