Skip to content
Draft
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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Observed entry points:
- `main.go`
- `api.New`
- `routes.RegisterRoutes`
- `api-spec/openapi.yaml`
- `api-spec/v0/openapi.yaml`

Observed deployment helpers:

Expand Down
31 changes: 8 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The Emmy API is a backend data service that connects to federal and commercial d

- **[About Emmy Software](https://cms.gov/eligibility-made-easy)**
- [Emmy API Overview](https://cms.gov/eligibility-made-easy)
- [Emmy Application Github](https://github.com/DSACMS/iv-cbv-payroll/blob/main/README.md)
- [Emmy Application GitHub](https://github.com/DSACMS/iv-cbv-payroll/blob/main/README.md)

- **[Technical Guide to Getting Started](docs/guides/01-getting-started.md)**
- [Usage Examples](docs/guides/03-usage-examples.md)
Expand All @@ -33,30 +33,12 @@ This project uses [pre-commit](https://pre-commit.com/ "pre-commit Docs") to reg
pre-commit install
```

For OpenAPI spec maintenance, the repo also includes utility scripts under
`scripts/`:
For OpenAPI spec maintenance, the supported local workflow is exposed through
`mise` tasks:

```sh
# 1. Rebuild the bundled YAML and JSON artifacts from the design-time source spec
./scripts/bundle-api-spec

# 2. Validate the bundled YAML artifact
./scripts/validate-api-spec

# 3. Lint the bundled YAML artifact with the repo Spectral ruleset
./scripts/lint-api-spec

# 4. Lint hand-authored YAML files
./scripts/lint-yaml-files

# 5. Check formatting for contract YAML and JSON files
./scripts/check-format-contract-files

# 6. Compile standalone JSON Schemas
./scripts/check-json-schemas

# 7. Check for breaking OpenAPI changes against a base ref
./scripts/check-openapi-breaking [base-ref]
# Run the full local contract/spec workflow
mise run check-contract-files
```

If you use [mise](https://mise.jdx.dev/), install the pinned runtimes from
Expand All @@ -74,6 +56,9 @@ You can also run each step individually with `mise run bundle-api-spec`,
`mise run validate-api-spec`, `mise run lint-api-spec`,
`mise run lint-yaml-files`, `mise run check-format-contract-files`,
`mise run check-json-schemas`, and `mise run check-openapi-breaking`.
The direct helper scripts currently present in `scripts/` are
`./scripts/lint-yaml-files`, `./scripts/check-format-contract-files`,
`./scripts/check-json-schemas`, and `./scripts/check-openapi-breaking`.

## Policies

Expand Down
16 changes: 9 additions & 7 deletions api-spec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,21 @@ The public contract in this branch is currently defined in

## Bundling And Checks

Use the repo scripts from the project root:
Use the supported local tasks from the project root:

```sh
./scripts/bundle-api-spec
./scripts/validate-api-spec
./scripts/lint-api-spec
mise install
mise run bundle-api-spec
mise run validate-api-spec
mise run lint-api-spec
```

Or, with `mise`:
Equivalent `pnpm` commands:

```sh
mise install
mise run check-api-spec
pnpm run bundle:api-spec
pnpm run validate:api-spec
pnpm run lint:api-spec
```

Bundling produces checked-in versioned artifacts under `api-spec/v0/dist/` and
Expand Down
140 changes: 62 additions & 78 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,100 +2,80 @@

## Overview

The server port is configured through `PORT` and defaults to `3000` in code;
the local example environment sets `PORT=8000`. The intended public API
contract for this branch is defined in `api-spec/v0/openapi.yaml`; this page
documents the currently wired Go runtime endpoints and their operational
caveats.

## Authentication Behavior

- When `SKIP_AUTH=false`, Cognito middleware is enabled globally.
- Middleware reads access token from header: `x-amzn-oidc-accesstoken`.
- Token checks include:
- valid signature via JWKS
- issuer match
- `token_use=access`
- `client_id` claim equals configured app client ID

If auth fails, response is `401 Unauthorized`.
This applies to `/api/edu` and `/api/v0/veteran-disability-ratings` when auth is
enabled. `/health` is registered before the auth middleware and remains
unauthenticated in the current branch.
The server port is configured through `PORT` and defaults to `3000` in code.
The local example environment sets `PORT=8000`. The intended public contract
for this branch is defined in `api-spec/v0/openapi.yaml`; this page documents
the currently wired Go runtime behavior.

## Circuit Breaker Behavior
## Request Identity Behavior

`/health`, `/api/edu`, and `/api/v0/veteran-disability-ratings` are wrapped by
Redis-backed circuit breaker middleware.
`api.New` always installs `SubjectMiddleware`, which populates `c.Locals("sub")`
using this precedence:

- On breaker deny/open state: `503 Service Unavailable`.
- On Redis state read failures with fail-open (default): request is allowed.
1. `X-Sub`
2. `Authorization: Bearer <JWT>` parsed without signature verification, using
only the token `sub` claim
3. fallback value `unknown-subject`

## Runtime Endpoints
When `SKIP_AUTH=true`, `SkipAuthMiddleware` is also installed and injects a
deterministic local identity before `SubjectMiddleware` runs. Optional override
headers are:

| Method | Path | Description | Success | Notes |
| ------ | ------------------------------ | -------------------------------------- | ----------- | ------------------------------------------------------------------ |
| `GET` | `/` | Liveness string | `200` text | Returns `Backend running!` |
| `GET` | `/health` | Redis health check | `200` empty | Registered before auth middleware; pings Redis with 2s timeout |
| `GET` | `/api/edu` | NSC education verification scaffold | `200` JSON | Uses a hardcoded request payload in handler; not the v0 contract |
| `POST` | `/api/v0/veteran-disability-ratings` | Veteran disability status from v0 spec | `200` JSON | Accepts caller-provided identity payload and matches the v0 route |

| Method | Path | Description | Success | Notes |
| ------ | --------------------- | ----------------------------------- | ----------- | ----- |
| `GET` | `/` | Liveness string | `200` text | Returns `Backend running!` |
| `GET` | `/status` | Redis health check | `200` empty | Uses 2s Redis ping timeout; wrapped by circuit breaker |
| `GET` | `/api-spec/v1/verify` | Bundled OpenAPI JSON artifact | `200` JSON | Returns `api-spec/dist/openapi.bundled.json` |
| `GET` | `/api/edu` | Education verification passthrough | `200` JSON | Uses hardcoded request payload in handler; wrapped by circuit breaker |

### NSC Submit Request model (`pkg/education/models_request.go`)

```go
type Request struct {
AccountID string `json:"accountId"`
OrganizationName string `json:"organizationName,omitempty"`
CaseReferenceID string `json:"caseReferenceId,omitempty"`
ContactEmail string `json:"contactEmail,omitempty"`
DateOfBirth string `json:"dateOfBirth"`
LastName string `json:"lastName"`
FirstName string `json:"firstName"`
SSN string `json:"ssn,omitempty"`
IdentityDetails []IdentityDetails `json:"identityDetails,omitempty"`
EndClient string `json:"endClient"`
PreviousNames []PreviousName `json:"previousNames,omitempty"`
Terms string `json:"terms"`
}
```
- `x-skip-auth-sub`
- `x-skip-auth-username`
- `x-skip-auth-scope`
- `x-skip-auth-groups`

### NSC Submit Response model (`pkg/education/models_response.go`)
The current branch does not wire an active Cognito verifier into the Fiber app.
Treat the checked-in v0 auth scheme as contract documentation rather than a
runtime-enforced guarantee.

```go
type Response struct {
ClientData ClientDataResponse `json:"clientData"`
IdentityDetails []IdentityDetailsResponse `json:"identityDetails"`
Status StatusResponse `json:"status"`
StudentInfoProvided StudentInfoProvidedResponse `json:"studentInfoProvided"`
TransactionDetails TransactionDetailsResponse `json:"transactionDetails"`
}
```
## Circuit Breaker Behavior

`GET /health`, `POST /api/v0/education-enrollments`, and
`POST /api/v0/veteran-disability-ratings` are wrapped by the Redis-backed
circuit-breaker middleware.

- Open breaker or breaker error response: `503 Service Unavailable`
- Redis read failures follow the breaker package's `FailOpen=true` default and
allow the request through

## Runtime Endpoints

| Method | Path | Description | Success | Notes |
|---|---|---|---|---|
| `GET` | `/` | Liveness string | `200` text | Returns `Backend running!` |
| `GET` | `/health` | Redis health check | `200` empty | Uses a 2-second Redis ping timeout and is circuit-breaker wrapped |
| `GET` | `/api-spec/v1/verify` | Bundled OpenAPI JSON artifact | `200` JSON | Serves `api-spec/v0/dist/openapi.bundled.json` |
| `POST` | `/api/v0/education-enrollments` | NSC education verification | `200` JSON | Parses caller JSON and requires `firstName`, `lastName`, and `dateOfBirth` |
| `POST` | `/api/v0/veteran-disability-ratings` | Veteran disability lookup | `200` JSON | Requires `firstName`, `lastName`, `dateOfBirth`, and either `ssn` or a full address |

## Example: `/health`

```bash
curl -i http://localhost:8000/health
```

### `/api-spec/v1/verify`
## Example: `/api-spec/v1/verify`

```bash
curl -i http://localhost:8000/api-spec/v1/verify
```

Returns the checked-in bundled OpenAPI JSON artifact with `Content-Type: application/json`.
Returns the checked-in bundled OpenAPI JSON artifact with
`Content-Type: application/json`.

## Example: `/api/edu` (auth skipped locally)
## Example: `/api/v0/education-enrollments`

```bash
curl -i http://localhost:8000/api/edu
curl -i --request POST http://localhost:8000/api/v0/education-enrollments \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <ACCESS_TOKEN>' \
--data '{
"firstName": "Lynette",
"lastName": "Oyola",
"dateOfBirth": "1988-10-24"
}'
```

## Example: `/api/v0/veteran-disability-ratings`
Expand All @@ -120,14 +100,18 @@ curl -i --request POST http://localhost:8000/api/v0/veteran-disability-ratings \

## Current-State Caveats

- `/api/edu` currently does not accept caller-provided payload; it submits a hardcoded sample request from handler code.
- `main` now injects Redis into `api.New`, so the health route has the Redis client it expects.
- The intended public contract for this branch is versioned under `api-spec/v0/`, and the veteran disability route matches that contract while `/api/edu` remains a runtime-only scaffold.
- Error response bodies come from Fiber error handling and may be plain text.
- The v0 OpenAPI contract documents OAuth 2.0 client credentials, but the
runtime currently uses `SubjectMiddleware` and optional skip-auth locals
instead of a verifying auth middleware.
- Error responses come from Fiber error handling and are plain text unless a
handler returns JSON explicitly.
- The education and veteran handlers add metadata such as `apiVersion`,
`environment`, timestamps, and datasource duration to successful responses.

## Assumptions

- **High confidence:** This page is a runtime reference, not the public API
contract reference.
- **Medium confidence:** `/api/edu` will be removed or reshaped as the runtime
converges on the published v0 contract.
- **High confidence:** `POST /api/v0/education-enrollments` and
`POST /api/v0/veteran-disability-ratings` are the only current runtime POST
endpoints under `/api/v0/`.
29 changes: 19 additions & 10 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ flowchart LR
## Interfaces and Abstractions

- `pkg/education/service.go`
- `type EducationService interface { Submit(ctx context.Context, req Request) (Response, error) }`
- `type Service interface { LookupEnrollmentStatus(ctx context.Context, req Request) (Response, error) }`
- `type HTTPTransport interface { Do(req *http.Request) (*http.Response, error) }`
- `pkg/veteran/service.go`
- `type Service interface { LookupDisabilityRating(ctx context.Context, req Request) (Response, error) }`
- `type HTTPTransport interface { Do(req *http.Request) (*http.Response, error) }`
- `pkg/circuitbreaker/circuitbreaker.go`
- `type Breaker interface { Allow; OnSuccess; OnFailure }`
Expand All @@ -62,7 +65,7 @@ without route-layer rewrites.
- `runServer` starts `app.Listen` in a goroutine and selects on server error or signal context cancellation.
- graceful shutdown uses `app.ShutdownWithTimeout(5 * time.Second)`.
- Request lifecycle:
- handlers create per-request contexts with timeout (`/health`: 2s, `/api/edu`: 5s, `/api/v0/veteran-disability-ratings`: 5s).
- handlers create per-request contexts with timeout (`/health`: 2s, `POST /api/v0/education-enrollments`: 30s, `POST /api/v0/veteran-disability-ratings`: 5s).
- Circuit-breaker middleware:
- breaker registry map guarded with `sync.RWMutex`.
- lazy breaker initialization via double-check lock pattern.
Expand All @@ -80,10 +83,12 @@ without route-layer rewrites.

Ordered middleware in `api.New`:

1. Recover
2. CORS (`*` origin/headers/methods)
3. Structured request logging (trace/span/request IDs)
4. Conditional Cognito auth middleware
1. Request ID context propagation
2. Structured request logging (trace/span/request IDs)
3. Recover
4. CORS (`*` origin/headers/methods)
5. Subject extraction middleware
6. Conditional local skip-auth identity injection

## Dependency Injection Pattern

Expand All @@ -99,10 +104,14 @@ injects it.

## Technical Caveats (Current State)

- `/api/edu` handler builds a hardcoded request payload instead of binding user input.
- `/health` is registered before the auth middleware, so it remains a runtime-only unauthenticated health route.
- Current runtime routes are a mix of scaffold and contract-aligned paths: `GET /`, `GET /health`, `GET /api/edu`, and `POST /api/v0/veteran-disability-ratings`.
- `GET /api/edu` remains runtime scaffolding, while `POST /api/v0/veteran-disability-ratings` matches the checked-in v0 contract in `api-spec/v0/openapi.yaml`.
- `api.New` does not currently install a Cognito-verifying middleware. The
request subject comes from `X-Sub`, an unverified bearer token `sub` claim,
or skip-auth locals when `SKIP_AUTH=true`.
- `GET /health` and the two `POST /api/v0/*` verification routes are
circuit-breaker wrapped, but `GET /` and `GET /api-spec/v1/verify` are not.
- The public contract documents OAuth 2.0 client-credentials auth, while the
runtime currently focuses on subject propagation plus downstream provider
calls.
- Some tests require local Redis and fail when unavailable.

## Assumptions
Expand Down
Loading
Loading