Skip to content

ammeep/avyapi

Repository files navigation

avyapi

A private HTTP API that returns normalized US avalanche forecasts from 30 avalanche centers. Single-purpose utility for the maintainer's own apps; not a public service.

Every request fetches live from the source (the center's own API, a parsed HTML page, or the NAC public API), validates against a canonical NAPADS schema, and caches at the edge. No database, no archive.

Quick start

npm install
cp .env.example .env   # set AVYAPI_KEY and CONTACT_EMAIL
npm start              # vercel dev on http://localhost:4000

Then:

curl -H "Authorization: Bearer $AVYAPI_KEY" \
  "http://localhost:4000/api/v1/forecast/KPAC?tz=America/Phoenix"

API

API routes namespaced under /api/v1/. Auth via Authorization: Bearer ${AVYAPI_KEY} or X-API-Key (/, /browse, and /api/v1/openapi.json excepted).

Endpoint Description
GET / Scalar API reference UI
GET /browse Click-through web UI for browsing centers, zones, forecasts
GET /api/v1/forecast/{center}[/{zone}][/{date}] Normalized forecast; requires ?tz= or X-Timezone
GET /api/v1/centers All known centers with strategy and capabilities
GET /api/v1/zones/{center} Zones for a center
GET /api/v1/openapi.json OpenAPI 3.1 spec

The OpenAPI spec is canonical — consult / for full schemas.

Coverage

30 US avalanche centers (Phase 2 complete, Phase 3 pending), by strategy:

Strategy Count Notes
api 1 CAIC (avid api-proxy)
html 13 Per-center HTML parsers (Phase 3)
nac-only 14 Centers delegating to NAC's widget
nac-special 1 CBAC (dedicated NAC endpoint)
unsupported 1 UAC — WAF-blocked, returns 501

The original HANDOFF estimated 3 api + 15 html centers based on May 2026 research. Phase 2 probes (PRs #4–#9) found that 5 of those candidates (SAC, MWAC, BTAC, WAC, FAC) actually publish their forecasts through the NAC widget rather than via a custom API or scraper-friendly HTML — they were downgraded to nac-only. CAIC stands as the only non-NAC center in v1. See each center's tests/fixtures/{centerId}/SOURCE.md for per-center investigation notes.

See centers.yaml for the full registry.

Adapter authoring pattern

The pattern that emerged from Phase 2:

1. Probe before parsing

Before assuming a center has a scrapeable forecast page:

  • Fetch the registry's forecastPagePattern URL once with our identifying UA (avyapi/{version} (+mailto:{CONTACT_EMAIL})).
  • Grep for nac-widget-loader.min.js or du6amfiq9m9h7.cloudfront.net — both indicate the center delegates to the NAC widget. If found, downgrade to nac-only (see below).
  • For WordPress centers, also probe /wp-json/wp/v2/types to look for a custom post type. If none and no advisory category has posts, it's not API-friendly.
  • Check tests/fixtures/nac/map-layer.json to confirm NAC already carries the center's zones with names matching the registry.

2. Downgrade to nac-only when possible

If the upstream is the NAC widget, edit centers.yaml:

strategy: nac-only
apiBaseUrl: null
capabilities: { danger: true, problems: false, rose: false, discussion: false }
napadsConformance: summary-only
supportsHistory: false

The existing NAC adapter (src/adapters/nac.ts) handles it automatically — no new TypeScript. Save a tests/fixtures/{centerId}/SOURCE.md documenting the probe findings, the redirect chains, and the in-season re-verification task.

3. When writing a real adapter

If the center has a genuine API or HTML forecast (so far: only CAIC):

  • Create src/adapters/{centerId}.ts implementing the Adapter interface (src/adapters/types.ts).
  • Read the URL from center.apiBaseUrl — don't hardcode it in the adapter.
  • fetch and parse are separate so a site redesign means editing only parse.
  • Throw the typed errors from types.ts (UpstreamBlockedError, UpstreamUnavailableError, UpstreamChangedError, HistoryUnsupportedError, NotFoundError) — the route handler maps them to the error envelope.
  • Always call isChallengeBody(res.text) on a 200 response before JSON.parse to distinguish a CDN block from a schema break.
  • Register in src/adapters/index.ts.
  • For full-capability centers (capabilities.problems: true), include an "is this a real forecast?" guard like CAIC's hasForecastContent (see src/adapters/caic.ts) — strict-mode validation will reject forecasts with all bands at -1 and empty problems.
  • Save a real fixture under tests/fixtures/{centerId}/. If only off-season data is available, also synthesize an in-season fixture for the strict-mode contract test.
  • Write a parse test plus a contract test that runs validateForecast in strict mode for in-season fixtures (the route runs strict for current requests).

Phase 2 follow-ups (Nov/Dec 2026 in-season verification)

  • CAIC: refresh tests/fixtures/caic/products-all-offseason.json with a real in-season capture; populate centers.yaml CAIC.zones (one entry per areaId, name from publicName or the Drupal slug); verify aspectElevations shape and finish mapProblem in caic.ts; retire tests/fixtures/caic/products-all-in-season-synthetic.json and point the strict contract test at the real fixture.
  • SAC, MWAC, BTAC, WAC, FAC: re-probe each center's forecast page. If any of them have moved off the NAC widget to inline forecast rendering, flip the registry strategy back to html (or api for MWAC/BTAC) and write a parser.
  • WAC season window: NAC reported WAC zones as off_season: False in June 2026; revisit seasonStart/seasonEnd if WAC publishes year-round travel advisories.
  • NAC fixtures: refresh tests/fixtures/nac/map-layer.json and tests/fixtures/nac/cbac.json with in-season captures so the lenient contract tests can move to strict.

See each center's tests/fixtures/{centerId}/SOURCE.md for per-center details.

Contributing / development

See HANDOFF.md for the build brief and architectural decisions, and CLAUDE.md for conventions and common commands.

License

MIT. Contact the maintainer via the CONTACT_EMAIL configured for the deployment (sent in the outbound User-Agent on every upstream fetch).

About

An API returning normalized US avalanche forecasts

Resources

License

Stars

Watchers

Forks

Contributors