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
38 changes: 38 additions & 0 deletions .github/workflows/pnpm-audit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: pnpm audit

on:
workflow_dispatch:
pull_request:
types:
- opened
- synchronize
- reopened
- ready_for_review
paths:
- package.json
- pnpm-lock.yaml

permissions:
contents: read

jobs:
audit:
name: audit
runs-on: self-hosted
steps:
- name: Fetch Repository
uses: actions/checkout@v6

- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: 10.32.1
run_install: false

- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: "lts/*"

- name: Run pnpm audit
run: pnpm audit --audit-level low
34 changes: 6 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,34 +29,8 @@ 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/`:

```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]
```

If you use [mise](https://mise.jdx.dev/), install the pinned runtimes from
`mise.toml` and run:
For OpenAPI spec maintenance, use the versioned `mise` tasks from the repo
root:

```sh
# Install the pinned Go and Node runtimes for this repo
Expand All @@ -71,6 +45,10 @@ You can also run each step individually with `mise run bundle-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 underlying Node commands are also available through `pnpm run
bundle:api-spec`, `pnpm run validate:api-spec`, `pnpm run lint:api-spec`,
`pnpm run check:fmt`, and `pnpm run check:schemas`.

## Policies

### Open Source Policy
Expand Down
18 changes: 11 additions & 7 deletions api-spec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,26 +42,30 @@ The public contract in this branch is currently defined in

## Bundling And Checks

Use the repo scripts from the project root:
Use the supported task runner commands from the project root:

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

Or, with `mise`:
To run the steps individually:

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

Bundling produces checked-in versioned artifacts under `api-spec/v0/dist/` and
`api-spec/dist/v0/`. Those artifacts are for machine consumption and runtime
serving; they should always be regenerated from the source files in this
directory.

The `mise` tasks call the repo's checked-in Node scripts, so the equivalent
lower-level commands are `pnpm run bundle:api-spec`, `pnpm run
validate:api-spec`, and `pnpm run lint:api-spec`.

## Pull Request Expectations

- Make contract intent clear in the PR description
Expand Down
93 changes: 41 additions & 52 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,21 @@ 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 checked-in v0 contract declares OAuth 2.0 bearer auth in
`api-spec/v0/openapi.yaml`.
- The current Go runtime on this branch does not register Cognito or bearer-token
verification middleware.
- When `SKIP_AUTH=true`, the app attaches `SkipAuthMiddleware`, which injects a
stable local identity into Fiber locals using optional `x-skip-auth-*`
override headers.
- `/health` is registered before that development middleware and remains
unauthenticated.

## Circuit Breaker Behavior

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

- On breaker deny/open state: `503 Service Unavailable`.
- On Redis state read failures with fail-open (default): request is allowed.
Expand All @@ -36,45 +34,29 @@ Redis-backed circuit breaker middleware.
| 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 |
| `GET` | `/health` | Redis health check | `200` empty | Registered before skip-auth middleware; pings Redis with 2s timeout |
| `GET` | `/api-spec/v1/verify` | Bundled OpenAPI JSON artifact | `200` JSON | Returns the checked-in spec artifact served by `OpenAPISpecHandler` |
| `POST` | `/api/v0/education-enrollments` | Education enrollment verification | `200` JSON | Parses caller JSON and returns `enrollmentStatus` |
| `POST` | `/api/v0/veteran-disability-ratings` | Veteran disability status from v0 spec | `200` JSON | Accepts caller-provided identity payload and returns rating data |

| 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`)
### Education 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"`
FirstName string `json:"firstName"`
MiddleName string `json:"middleName,omitempty"`
LastName string `json:"lastName"`
DateOfBirth string `json:"dateOfBirth"`
SSN string `json:"ssn,omitempty"`
Address *Address `json:"address,omitempty"`
}
```

### NSC Submit Response model (`pkg/education/models_response.go`)
### Education response model (`pkg/education/models_response.go`)

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

Expand All @@ -84,26 +66,33 @@ type Response struct {
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 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' \
--data '{
"firstName": "Lynette",
"lastName": "Oyola",
"dateOfBirth": "1988-10-24",
"ssn": "123-45-6789"
}'
```

## Example: `/api/v0/veteran-disability-ratings`

```bash
curl -i --request POST http://localhost:8000/api/v0/veteran-disability-ratings \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <ACCESS_TOKEN>' \
--data '{
"firstName": "Lynette",
"lastName": "Oyola",
Expand All @@ -120,14 +109,14 @@ 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.
- The intended public contract for this branch is versioned under `api-spec/v0/`, and both versioned POST routes are registered in the current runtime.
- The current runtime docs do not imply active bearer-token enforcement because no Cognito or bearer verification middleware is wired on this branch.
- Error response bodies come from Fiber error handling and may be plain text.

## 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.
- **Medium confidence:** The checked-in contract and runtime auth behavior are
still converging, so future auth-related docs may need another refresh.
22 changes: 11 additions & 11 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ 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/core/otel.go`
- `type OtelService interface { SpanFromContext; LoggerProvider; Shutdown }`
Expand All @@ -64,7 +64,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, `/api/v0/education-enrollments`: 30s, `/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 @@ -82,11 +82,12 @@ without route-layer rewrites.

Ordered middleware in `api.New`:

1. Recover
2. CORS (`*` origin/headers/methods)
3. OpenTelemetry Fiber middleware
4. Structured request logging (trace/span/request IDs)
5. Conditional Cognito auth middleware
1. Request ID propagation
2. Structured request logging (trace/span/request IDs)
3. Recover
4. CORS (`*` origin/headers/methods)
5. OpenTelemetry Fiber middleware
6. Optional `SkipAuthMiddleware` when `SKIP_AUTH=true`

## Dependency Injection Pattern

Expand All @@ -102,10 +103,9 @@ 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`.
- `/health` is registered before the optional skip-auth middleware, so it remains unauthenticated in the current runtime.
- Current runtime routes are `GET /`, `GET /health`, `GET /api-spec/v1/verify`, `POST /api/v0/education-enrollments`, and `POST /api/v0/veteran-disability-ratings`.
- The education and veteran handlers both accept caller-provided JSON bodies, but the checked-in OpenAPI contract still documents auth expectations separately from the currently wired Go middleware stack.
- Some tests require local Redis and fail when unavailable.

## Assumptions
Expand Down
Loading
Loading