mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 08:11:10 +00:00
[AZ-497] [AZ-498] [AZ-499] Cycle 2 New Task: epic, stories, contract draft
Closes autodev existing-code Step 9 for cycle 2.
- Epic AZ-497 (Self-Hosted Satellite Tiles - SPA Integration) added
to _docs/02_tasks/_dependencies_table.md as the cycle-2 umbrella.
- AZ-498 (5 pts): self-hosted satellite tiles + drop map-type toggle.
Cross-workspace prereq: satellite-provider must add cookie-auth on
GET /tiles/{z}/{x}/{y} before merge (user files separately).
- AZ-499 (2 pts): mission-planner OWM env-var hardening + closes the
AZ-482 source-scan gap with a new owm_key_in_source banned-deps kind.
- Contract _docs/02_document/contracts/satellite-provider/tiles.md
v1.0.0 (draft): slippy-tile XYZ shape both sides commit to.
- _docs/_autodev_state.md: Step 9 closure note + advances pointer to
Step 10 (Implement) for cycle 2.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
# Contract: satellite-provider tile serving
|
||||
|
||||
**Component**: satellite-provider
|
||||
**Producer task**: TBD — separate AZAION ticket on `satellite-provider` workspace (user-filed)
|
||||
**Consumer tasks**: AZ-498 — `_docs/02_tasks/todo/AZ-498_satellite_tile_swap.md` (suite/ui, cycle 2, epic AZ-497)
|
||||
**Version**: 1.0.0
|
||||
**Status**: draft
|
||||
**Last Updated**: 2026-05-12
|
||||
|
||||
## Purpose
|
||||
|
||||
Describe the slippy-tile HTTP interface that the suite UI consumes to render
|
||||
satellite imagery in `FlightMap` / `MiniMap`. Replaces the prior external-tile
|
||||
dependencies (OpenStreetMap, Esri ArcGIS World Imagery). The endpoint is
|
||||
served by `SatelliteProvider.Api` and backed by an on-disk + Google-Maps
|
||||
download cache.
|
||||
|
||||
Frozen post-migration: SPA authentication for this endpoint MUST be **cookie-based**
|
||||
(JWT delivered via `HttpOnly; Secure; SameSite=Lax` cookie on the same origin)
|
||||
because Leaflet's `<TileLayer>` issues plain `<img>` requests and cannot attach
|
||||
`Authorization: Bearer …` headers.
|
||||
|
||||
## Shape
|
||||
|
||||
### HTTP / RPC endpoints
|
||||
|
||||
| Method | Path | Request body | Response | Status codes |
|
||||
|--------|-------------------------------|--------------|-------------------|---------------------|
|
||||
| `GET` | `/tiles/{z}/{x}/{y}` | — | image bytes | 200, 401, 404, 503 |
|
||||
|
||||
**Path parameters**
|
||||
|
||||
| Name | Type | Required | Range / Constraint |
|
||||
|------|---------|----------|--------------------------------------------------------|
|
||||
| `z` | `int` | yes | `0 ≤ z ≤ 20` (slippy-tile zoom) |
|
||||
| `x` | `int` | yes | `0 ≤ x < 2^z` (slippy-tile column) |
|
||||
| `y` | `int` | yes | `0 ≤ y < 2^z` (slippy-tile row, TMS-y convention NO) |
|
||||
|
||||
Coordinates follow the Google Maps / OSM XYZ tiling scheme (NOT the inverted TMS
|
||||
y-axis). Out-of-range coordinates SHOULD return 404.
|
||||
|
||||
**Response headers (on 200)**
|
||||
|
||||
| Header | Value |
|
||||
|------------------|---------------------------------------------------------------|
|
||||
| `Content-Type` | `image/jpeg` (image bytes from the `TileService`) |
|
||||
| `Cache-Control` | `public, max-age=N` where N is set by `TileService` |
|
||||
| `ETag` | strong ETag tied to the cached tile's content hash |
|
||||
|
||||
**Authentication**
|
||||
|
||||
- **Required**: yes (the endpoint is NOT public).
|
||||
- **Mechanism (post-migration)**: cookie-based JWT.
|
||||
- Cookie name: `satellite_auth` (TBD — defined by producer task).
|
||||
- Attributes: `HttpOnly; Secure; SameSite=Lax` in production; `SameSite=Lax`
|
||||
permitted over `http://localhost` for dev only.
|
||||
- **Cross-origin behavior**: same-origin only. The SPA reaches this endpoint via
|
||||
the suite ingress (nginx) on the SPA's origin; cross-origin direct calls from
|
||||
`http://localhost:5173 → http://localhost:5100` will NOT carry the cookie and
|
||||
will receive 401 in dev unless the developer disables auth locally.
|
||||
|
||||
**Status codes**
|
||||
|
||||
| Code | Meaning |
|
||||
|------|-------------------------------------------------------------------|
|
||||
| 200 | Cached or freshly downloaded tile; body = image bytes |
|
||||
| 304 | (Optional) ETag match — body empty. UI MUST tolerate either 200 or 304. |
|
||||
| 401 | Missing/invalid cookie — UI MUST treat as "user signed out" |
|
||||
| 404 | Tile coordinates out of range OR upstream had no tile |
|
||||
| 503 | Upstream (Google Maps) unavailable; UI MUST render placeholder |
|
||||
|
||||
## Invariants
|
||||
|
||||
- The endpoint URL pattern is `/tiles/{z}/{x}/{y}` exactly — never `/tiles/{z}/{y}/{x}`
|
||||
(Esri-style) nor `/api/satellite/tiles/{z}/{x}/{y}`. This invariant survives
|
||||
refactors and is asserted by both producer's integration tests and consumer's
|
||||
blackbox tests.
|
||||
- Image format is JPEG (Content-Type `image/jpeg`). Switching to PNG/WEBP is a
|
||||
major-version change.
|
||||
- The endpoint MUST honor `Cache-Control` and `ETag` headers on every 200; clients
|
||||
rely on them to avoid re-fetching unchanged tiles during pan/zoom.
|
||||
- Authentication failure MUST return 401, not 200 with an HTML body — Leaflet
|
||||
would otherwise display a broken-image placeholder silently.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Not covered: tile vector formats (`.pbf` / Mapbox Vector Tiles). This contract
|
||||
is raster-only.
|
||||
- Not covered: tile prewarming. Pre-warm uses the separate `POST /api/satellite/request`
|
||||
endpoint (different contract, not consumed by the UI's `FlightMap`).
|
||||
- Not covered: MGRS tile retrieval (returns 501 today; out of UI scope).
|
||||
|
||||
## Versioning Rules
|
||||
|
||||
- **Breaking** (major bump): change the path template, change the path-parameter
|
||||
semantics (e.g., TMS-y), change `Content-Type`, remove a status code from the
|
||||
set above, change the auth mechanism away from cookies.
|
||||
- **Non-breaking** (minor bump): add a new optional query parameter, broaden the
|
||||
zoom range, add a new status code in the 4xx/5xx space that consumers can
|
||||
tolerate.
|
||||
|
||||
## Test Cases
|
||||
|
||||
| Case | Input | Expected | Notes |
|
||||
|----------------------------|----------------------------------------|-----------------------------------------------------------|----------------------------------|
|
||||
| valid-tile | `GET /tiles/15/9876/5432` w/ cookie | 200 + JPEG bytes + `Cache-Control` + `ETag` | producer + consumer cover |
|
||||
| missing-cookie | `GET /tiles/15/9876/5432` w/o cookie | 401 | consumer must NOT retry |
|
||||
| out-of-range-coord | `GET /tiles/3/8/0` (x ≥ 2^z) | 404 | consumer renders placeholder |
|
||||
| etag-match | `GET /tiles/15/9876/5432` + `If-None-Match` | 304 OR 200 (server-policy dependent) | consumer tolerates both |
|
||||
| upstream-503 | upstream Google Maps down | 503 | consumer renders placeholder |
|
||||
| zoom-extreme | `GET /tiles/20/x/y` valid coords | 200 (or 404 if not cached and no on-demand) | consumer caps zoom at 20 |
|
||||
|
||||
## Change Log
|
||||
|
||||
| Version | Date | Change | Author |
|
||||
|---------|------------|------------------------------------------------------------------------------|--------|
|
||||
| 1.0.0 | 2026-05-12 | Initial draft; freezes the post-migration shape (cookie auth, XYZ scheme). | autodev (cycle 2 — suite/ui) |
|
||||
@@ -78,3 +78,20 @@
|
||||
- `e2e (requires-docker)`: AZ-480 — requires the suite docker-compose stack
|
||||
- `e2e (requires-ci)`: AZ-481 NFT-RES-LIM-12/13 — local skip allowed
|
||||
- **Quarantine scenarios**: FT-P-12 (async video detect, AZ-461) starts QUARANTINEd until AC-25 / Phase B; verification_pending enums in AZ-459 quarantine until Step 4 .NET-service snapshot lifts.
|
||||
|
||||
---
|
||||
|
||||
## Epic AZ-497 — Self-Hosted Satellite Tiles — SPA Integration (cycle 2)
|
||||
|
||||
| Task | Name | Epic | Complexity | Depends on |
|
||||
|------|------|------|-----------|------------|
|
||||
| AZ-498 | Self-hosted satellite tiles + drop map-type toggle | AZ-497 | 5 | AZ-450; cross-workspace: satellite-provider cookie-auth (user-filed) |
|
||||
| AZ-499 | mission-planner OWM env-var hardening + AZ-482 source-scan gap | AZ-497 | 2 | AZ-448, AZ-449, AZ-482 |
|
||||
|
||||
### Notes (AZ-497)
|
||||
|
||||
- **Epic AZ-497** is the cycle-2 umbrella selected by the user during the autodev new-task session. It covers BOTH the SPA-side tile swap to `satellite-provider` (AZ-498) and the `mission-planner` OWM hardening (AZ-499). The OWM work is not literally about satellite tiles; the user explicitly accepted the wider umbrella to avoid creating a second cycle-2 epic.
|
||||
- **AZ-498 — cross-workspace dependency**: requires `satellite-provider` to expose a cookie-auth variant of `GET /tiles/{z}/{x}/{y}` before merge. The user files that ticket on the satellite-provider workspace separately. UI work can be authored ahead but cannot ship without the upstream change.
|
||||
- **AZ-498 — contract**: produces/consumes `_docs/02_document/contracts/satellite-provider/tiles.md` (v1.0.0, draft).
|
||||
- **AZ-499 — out-of-band**: the compromised key `335799082893fad97fa36118b131f919` must be revoked at the OpenWeatherMap dashboard before AZ-499 closes. AC-7 captures that as a deliverable.
|
||||
- **AZ-499 — gap fix**: adds a new `owm_key_in_source` banned-deps kind that covers `src/` AND `mission-planner/`, closing the source-scan gap left by AZ-482's `dist/`-only scan.
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
# Replace external map tiles with self-hosted satellite-provider
|
||||
|
||||
**Task**: AZ-498_satellite_tile_swap
|
||||
**Name**: Self-hosted satellite tiles + drop map-type toggle
|
||||
**Description**: Replace OpenStreetMap (classic) and Esri (satellite) tile sources with the suite's own `satellite-provider /tiles/{z}/{x}/{y}` endpoint, drop the classic/satellite toggle (satellite-provider serves satellite imagery only), and wire cookie-based authentication for tile fetches.
|
||||
**Complexity**: 5 points
|
||||
**Dependencies**: AZ-450 (Externalize map tile URLs). Cross-workspace prerequisite — satellite-provider must publish a cookie-auth variant of `/tiles/{z}/{x}/{y}` before this task can be merged. The user files that ticket separately on the satellite-provider workspace.
|
||||
**Component**: 05_flights (with adjustments to 10_app-shell and the e2e harness)
|
||||
**Tracker**: AZ-498
|
||||
**Epic**: AZ-497
|
||||
|
||||
## Problem
|
||||
|
||||
`src/features/flights/types.ts` (post AZ-450) reads two tile-URL env vars and exposes them to `FlightMap` and `MiniMap` via a `{ classic, satellite }` shape. Today those URLs resolve to external providers (OpenStreetMap, Esri ArcGIS World Imagery). This:
|
||||
|
||||
- Sends pilot flight-area coordinates to third-party CDNs (privacy/operational risk for sensitive missions).
|
||||
- Adds an external network dependency the air-gap NFR (NFT-RES-03 / restriction E1) was meant to eliminate — the e2e profile only papers over it via the `tile-stub`.
|
||||
- Wastes bandwidth re-downloading tiles that the suite's own `satellite-provider` service already caches on disk (`./tiles/{z}/{x}/{y}.jpg`).
|
||||
|
||||
The suite already runs a `satellite-provider` .NET service that exposes a slippy-tile XYZ endpoint (`GET /tiles/{z}/{x}/{y}`) backed by an on-disk cache plus on-demand Google Maps download, with `Cache-Control` and `ETag` headers wired. The UI does not consume it.
|
||||
|
||||
## Outcome
|
||||
|
||||
- The SPA's map renders satellite tiles served by the suite's own `satellite-provider`, on the same origin as the SPA in production.
|
||||
- The classic/satellite toggle is removed; the map is satellite-only.
|
||||
- Tile fetches authenticate via a same-origin cookie, not via an `Authorization: Bearer …` header (Leaflet `<img>` requests cannot send the header).
|
||||
- Air-gap restriction E1 is satisfied for tiles in production without requiring a stub.
|
||||
- `_docs/02_document/contracts/satellite-provider/tiles.md` documents the contract both sides commit to.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
|
||||
- Collapse `TILE_URLS` in `src/features/flights/types.ts` to a single URL string read from `import.meta.env.VITE_SATELLITE_TILE_URL`.
|
||||
- Remove the classic/satellite toggle from `FlightMap.tsx`: the `mapType` state, the toggle `<button>`, and the `mapType` prop passed to `MiniMap`.
|
||||
- Update `MiniMap.tsx` to render a single `<TileLayer>` without a `mapType` prop.
|
||||
- Both `<TileLayer>` instances MUST include `crossOrigin="use-credentials"` so the browser attaches the auth cookie on same-origin requests.
|
||||
- Update `.env.example`: add `VITE_SATELLITE_TILE_URL`, remove `VITE_OSM_TILE_URL` and `VITE_ESRI_TILE_URL`, refresh the comment block.
|
||||
- Update `src/vite-env.d.ts`: add `VITE_SATELLITE_TILE_URL?: string`, remove the two OSM/Esri declarations.
|
||||
- Update `_docs/02_document/contracts/satellite-provider/tiles.md` to reference this task in the `Consumer tasks` field once the ticket ID is assigned.
|
||||
- Update `e2e/docker-compose.suite-e2e.yml`: replace `tile-stub` wiring with either (a) a redirect of the SPA's `VITE_SATELLITE_TILE_URL` to the actual `satellite-provider` Docker service, or (b) repurpose `e2e/stubs/tile/server.ts` to serve the `/tiles/{z}/{x}/{y}` path used by the new contract. The choice is made during implementation to minimize churn in the e2e harness.
|
||||
- Update `e2e/tests/infrastructure.e2e.ts` AC-2 path assertion and `e2e/tests/tile_split_zoom.e2e.ts` to point at the new path/host.
|
||||
- Remove the i18n key `flights.planner.satellite` from `src/i18n/en.json` and `src/i18n/ua.json` (the toggle that referenced it is gone). Verify no other call site references the key.
|
||||
- Update `_docs/02_document/modules/src__features__flights.md` and `_docs/02_document/components/05_flights/description.md` to reflect the new tile source and the removed toggle.
|
||||
- New blackbox test that asserts the `<TileLayer>` URL resolves to the env-var value AND that `crossOrigin="use-credentials"` is present on the rendered DOM element.
|
||||
- New blackbox test that asserts the toggle button and the `mapType` state are absent from the rendered `FlightMap`.
|
||||
|
||||
### Excluded
|
||||
|
||||
- The `satellite-provider` server-side change to switch `/tiles/{z}/{x}/{y}` from JWT bearer to cookie authentication. Filed separately on the satellite-provider workspace; this task assumes that work lands first.
|
||||
- Bringing back any street-tile fallback. Re-introducing OSM-style classic view is a future task.
|
||||
- Pre-warming tile caches via `POST /api/satellite/request`. The SPA does not call that endpoint; on-demand server-side cache fill is sufficient.
|
||||
- Refactoring `mission-planner/` map tiles. Task 2 handles `mission-planner` separately for OWM, and `mission-planner`'s tile config is independent (its own `VITE_SATELLITE_TILE_URL`).
|
||||
- Adding `If-None-Match` / 304 handling on the consumer side. Leaflet's built-in caching is sufficient.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
**AC-1: Single env-var resolves the tile URL**
|
||||
Given `VITE_SATELLITE_TILE_URL=http://satellite-provider:5100/tiles/{z}/{x}/{y}` at build time,
|
||||
When `FlightMap` mounts,
|
||||
Then the rendered `<TileLayer>` `url` prop equals that exact string.
|
||||
|
||||
**AC-2: Default URL when env var is unset**
|
||||
Given `VITE_SATELLITE_TILE_URL` is unset at build time,
|
||||
When the bundle runs,
|
||||
Then `<TileLayer>` `url` resolves to `http://localhost:5100/tiles/{z}/{x}/{y}` (dev default, per the cycle-2 assumption-validation decision).
|
||||
|
||||
**AC-3: Cookie auth is wired**
|
||||
Given the satellite-provider expects an `HttpOnly; SameSite=Lax` cookie,
|
||||
When `<TileLayer>` issues a tile request via Leaflet,
|
||||
Then the rendered `<img>` element exposes `crossOrigin="use-credentials"` so the browser sends the cookie on same-origin requests.
|
||||
|
||||
**AC-4: Map-type toggle removed**
|
||||
Given `FlightMap` mounts,
|
||||
When the user inspects the rendered output,
|
||||
Then there is no toggle button, no `mapType` state, and `MiniMap`'s `Props` no longer accepts a `mapType` value.
|
||||
|
||||
**AC-5: Env declarations stay in sync**
|
||||
Given a TypeScript build,
|
||||
Then `ImportMetaEnv` declares only `VITE_SATELLITE_TILE_URL` (the two prior OSM/Esri vars are gone), and `.env.example` lists `VITE_SATELLITE_TILE_URL` in the same documented style.
|
||||
|
||||
**AC-6: E2E suite-e2e harness exercises the new path**
|
||||
Given the e2e profile is brought up via `e2e/docker-compose.suite-e2e.yml`,
|
||||
When the harness asserts the tile endpoint via `infrastructure.e2e.ts` AC-2,
|
||||
Then the request URL is `http://<tile-host>:<port>/tiles/{z}/{x}/{y}` (not `/{z}/{x}/{y}.png` and not the `/sat/...` Esri shape), and the response is a 256×256 image.
|
||||
|
||||
**AC-7: Contract documented**
|
||||
Given `_docs/02_document/contracts/satellite-provider/tiles.md` exists,
|
||||
When `code-review` Phase 2 runs against this task,
|
||||
Then the contract's `Shape` section matches the URL pattern and headers used by the rendered `<TileLayer>` and assert no `Spec-Gap` finding.
|
||||
|
||||
**AC-8: Legacy tile-aware tests still pass**
|
||||
Given `tests/tile_split_zoom.test.tsx` and `e2e/tests/tile_split_zoom.e2e.ts` are updated to the new URL,
|
||||
When the test suite runs,
|
||||
Then both tests pass against the new tile-URL shape.
|
||||
|
||||
**AC-9: Architecture gate stays green**
|
||||
Given the static-only profile runs (`scripts/run-tests.sh --static-only`),
|
||||
When `STC-ARCH-01` and `STC-ARCH-02` execute,
|
||||
Then no new cross-component import violation is introduced.
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
**Performance**
|
||||
|
||||
- A cold pan over an uncached region must not block the UI thread: the SPA must continue to render placeholders while `satellite-provider` downloads upstream tiles.
|
||||
- The same tile URL viewed twice within a session MUST be served from the browser's HTTP cache (i.e., `Cache-Control` + `ETag` round-trip).
|
||||
|
||||
**Compatibility**
|
||||
|
||||
- The `MapContainer` / `TileLayer` API surface in `react-leaflet` is unchanged. No version bump.
|
||||
- Production deploy MUST work behind the suite's nginx ingress on a single origin; cross-origin direct calls are explicitly NOT supported.
|
||||
|
||||
**Reliability**
|
||||
|
||||
- A 401 from the tile endpoint MUST NOT crash the map; it must render a broken-tile placeholder and the rest of the SPA must remain functional.
|
||||
- A 503 from the tile endpoint (Google Maps upstream down) MUST be tolerated identically to 404.
|
||||
|
||||
## Unit Tests
|
||||
|
||||
| AC Ref | What to Test | Required Outcome |
|
||||
|--------|-------------|-----------------|
|
||||
| AC-1 | Module-scope evaluation of `TILE_URL` with env mocked | Equals mocked value |
|
||||
| AC-2 | Module-scope evaluation of `TILE_URL` with env unset | Equals dev default |
|
||||
| AC-5 | TypeScript compilation against `ImportMetaEnv` | Compiles; no `VITE_OSM_TILE_URL` reference remains |
|
||||
|
||||
## Blackbox Tests
|
||||
|
||||
| AC Ref | Initial Data/Conditions | What to Test | Expected Behavior | NFR References |
|
||||
|--------|-------------------------|--------------|-------------------|----------------|
|
||||
| AC-1 / AC-2 | env set / unset; `FlightMap` mounted | Rendered `<TileLayer>` `url` prop | Equals the resolved URL | Compat |
|
||||
| AC-3 | `FlightMap` mounted | Rendered tile `<img>` element's `crossOrigin` attribute | `use-credentials` | Reliability |
|
||||
| AC-4 | `FlightMap` mounted | DOM scan for `[data-testid="map-type-toggle"]` AND absence of `mapType` references | Toggle absent; `MiniMap.Props` has no `mapType` | UX |
|
||||
| AC-6 | suite-e2e profile up | GET `http://<tile-host>:<port>/tiles/1/0/0` | 200 + image bytes | E2E determinism |
|
||||
| AC-7 | Contract file present | Contract shape matches implementation | No `Spec-Gap` finding | Docs |
|
||||
| AC-8 | tile_split_zoom tests updated | Run against new URL shape | Pass | Compat |
|
||||
|
||||
## Constraints
|
||||
|
||||
- Leaflet's `<TileLayer>` API surface MUST NOT change; only the `url` value, `crossOrigin` prop, and removal of the per-mode branching change.
|
||||
- Same-origin deployment via nginx is the production assumption. Any setup that requires cross-origin cookies on tile requests is out of scope.
|
||||
- No new third-party tile provider may be introduced as a fallback (would re-violate restriction E1).
|
||||
|
||||
## Risks & Mitigation
|
||||
|
||||
**Risk 1: Cross-workspace dependency (cookie auth on `/tiles/{z}/{x}/{y}`)**
|
||||
- *Risk*: Until the satellite-provider workspace adds cookie auth, the endpoint returns 401 to the SPA in production. Merging the UI side first results in a broken map.
|
||||
- *Mitigation*: The user files the satellite-provider-side ticket separately. This UI task is gated on that work landing. The task's deploy step (autodev Step 16) MUST verify both sides are in place before flipping prod traffic; suggested gate is a "tiles-render" smoke check in the deploy skill.
|
||||
|
||||
**Risk 2: Dev environment cookie scope (`localhost:5173` ↔ `localhost:5100`)**
|
||||
- *Risk*: Once cookie auth is enforced, devs running the SPA at `localhost:5173` and satellite-provider at `localhost:5100` cannot send the auth cookie cross-port. Tiles will 401 in dev.
|
||||
- *Mitigation*: Document the limitation in `_docs/02_document/deployment/environment_strategy.md`. Recommend local satellite-provider be run with auth disabled OR be reached through the suite's local nginx (same origin). This is an explicit trade-off the user accepted at cycle-2 assumption validation.
|
||||
|
||||
**Risk 3: UX regression — losing the classic (street) view**
|
||||
- *Risk*: Pilots accustomed to OSM road context for ground-reference lose that view.
|
||||
- *Mitigation*: Accepted by user choice (cycle-2 tile-scope = B). Tracked here so a future cycle can restore a street view via a different self-hosted source if demand arises.
|
||||
|
||||
**Risk 4: E2E flake during the tile-stub repurpose**
|
||||
- *Risk*: Repurposing `e2e/stubs/tile/server.ts` to the new path may cause AZ-456 / AZ-474 / AZ-479 / AZ-480 e2e tests to flap during the transition.
|
||||
- *Mitigation*: Land the suite-e2e compose change in the same PR as the source change so the harness is consistent in every commit. Add a short pre-flight check in `infrastructure.e2e.ts` that confirms the stub responds at the new path before downstream specs run.
|
||||
|
||||
**Risk 5: Silent broken-image rendering on auth failure**
|
||||
- *Risk*: If cookie auth fails post-deploy, Leaflet renders blank tiles without surfacing a user-facing error.
|
||||
- *Mitigation*: Add a `tileerror` listener on the `<MapContainer>` that, on the first error, logs a structured warning and (optionally) shows an inline banner ("Imagery unavailable; please re-sign-in"). This is a small follow-up; recommended as part of this task's deliverables but acceptable to defer to a follow-up if scope pressure builds.
|
||||
|
||||
## Contract
|
||||
|
||||
This task consumes the contract at `_docs/02_document/contracts/satellite-provider/tiles.md` (v1.0.0, status: draft).
|
||||
The satellite-provider workspace owns producing/maintaining that contract. The UI MUST read that file — not this task spec — to discover the interface.
|
||||
|
||||
### Document Dependencies
|
||||
|
||||
- `_docs/02_document/contracts/satellite-provider/tiles.md` — slippy-tile API contract.
|
||||
- `_docs/02_document/components/05_flights/description.md` — owning component description.
|
||||
- `_docs/02_document/modules/src__features__flights.md` — module-layout mapping for the affected files.
|
||||
@@ -0,0 +1,143 @@
|
||||
# Externalize mission-planner OWM key + base URL; close AZ-482 source-scan gap
|
||||
|
||||
**Task**: AZ-499_mission_planner_weather_env
|
||||
**Name**: mission-planner OWM env-var hardening
|
||||
**Description**: Replace the hardcoded OpenWeatherMap API key and base URL in `mission-planner/src/services/WeatherService.ts` with Vite env vars (mirroring AZ-448 / AZ-449 on the main SPA), and close the AZ-482 source-scan gap that previously allowed the committed key to slip past the static check.
|
||||
**Complexity**: 2 points
|
||||
**Dependencies**: AZ-448 (Externalize OWM API key), AZ-449 (Externalize OWM base URL), AZ-482 (Secrets/banned-libs static check).
|
||||
**Component**: 05_flights (mission-planner port-root)
|
||||
**Tracker**: AZ-499
|
||||
**Epic**: AZ-497
|
||||
|
||||
## Problem
|
||||
|
||||
`mission-planner/src/services/WeatherService.ts` lines 4–5 contain:
|
||||
|
||||
```ts
|
||||
const apiKey = '335799082893fad97fa36118b131f919';
|
||||
const url = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${apiKey}&units=metric`;
|
||||
```
|
||||
|
||||
Two issues:
|
||||
|
||||
1. **Compromised secret in source**: a real OpenWeatherMap API key is committed and has been in git history. Anyone with read access to the repo (or to any past mirror) can grab and abuse it.
|
||||
2. **Hygiene gap**: AZ-448/AZ-449 closed the same pattern on the main SPA (`src/features/flights/flightPlanUtils.ts`), and AZ-482 was supposed to keep the key out via a static check. But AZ-482's `owm_key_in_dist` kind only scans the post-build `dist/` artifact, not the source tree, and only the main SPA bundle (not `mission-planner/`). STC-S5 keeps `mission-planner/` out of `dist/`, so today the key never reaches the bundle — but it remains plainly visible in source and survives every test run.
|
||||
|
||||
## Outcome
|
||||
|
||||
- `mission-planner/src/services/WeatherService.ts` reads `VITE_OWM_API_KEY` and `VITE_OWM_BASE_URL` from `import.meta.env`; never references the literal key.
|
||||
- `getWeatherData` returns `null` when `VITE_OWM_API_KEY` is unset (same fail-soft contract as AZ-448 on the main SPA).
|
||||
- `mission-planner/.env.example` and `mission-planner/src/vite-env.d.ts` declare both vars.
|
||||
- A new banned-deps kind `owm_key_in_source` scans `src/` AND `mission-planner/` for the (now-rotated) old key literal and any future hardcoded fallback. STC-S? wires it into `scripts/run-tests.sh --static-only`.
|
||||
- The compromised key `335799082893fad97fa36118b131f919` is revoked at the OpenWeatherMap dashboard out-of-band, before this task closes. The revocation is a deliverable, not just a recommendation.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
|
||||
- `mission-planner/src/services/WeatherService.ts`: replace the two literals with `import.meta.env.VITE_OWM_API_KEY` and `import.meta.env.VITE_OWM_BASE_URL`; when the key is unset, return `null` without calling `fetch`.
|
||||
- `mission-planner/.env.example`: add `VITE_OWM_API_KEY=<your-openweathermap-api-key>` and `VITE_OWM_BASE_URL=https://api.openweathermap.org/data/2.5`; mirror the docstring style of the main `.env.example`.
|
||||
- `mission-planner/src/vite-env.d.ts`: add `VITE_OWM_API_KEY?: string` and `VITE_OWM_BASE_URL?: string`.
|
||||
- `tests/security/banned-deps.json`: add a new `owm_key_in_source` kind:
|
||||
- `ac`: NFT-SEC-09 (AC-1, source portion) — OpenWeatherMap key not present in source tree
|
||||
- `scope`: `src/ and mission-planner/ (production sources; tests excluded)`
|
||||
- `match`: `literal`
|
||||
- `patterns`: `["335799082893fad97fa36118b131f919"]`
|
||||
- `scripts/run-tests.sh`: add a new static-check row (e.g., `STC-S6`) that wires the new kind via `node scripts/check-banned-deps.mjs --kind=owm_key_in_source`.
|
||||
- `_docs/02_document/modules/mission-planner.md` (or the closest existing mission-planner doc): note the env-var dependency under the WeatherService entry.
|
||||
- Manual out-of-band: revoke the compromised key at `https://home.openweathermap.org/api_keys`; provision the new key in CI/dev `.env.local` for mission-planner.
|
||||
|
||||
### Excluded
|
||||
|
||||
- The broader F1 mission-planner deduplication work — tracked under its own future epic per `_docs/02_tasks/_dependencies_table.md` notes; this task is narrow security hygiene, not the duplication fix.
|
||||
- Adding tests for `getWeatherData`'s `WeatherData` mapping logic (existing behavior, no test coverage today; out of scope here).
|
||||
- Changing `getWeatherData`'s public signature.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
**AC-1: Env-var resolved API key**
|
||||
Given `VITE_OWM_API_KEY=abc123` at build time,
|
||||
When `getWeatherData(lat, lon)` is invoked,
|
||||
Then the outgoing `fetch` URL contains `appid=abc123` and `units=metric`.
|
||||
|
||||
**AC-2: Env-var resolved base URL**
|
||||
Given `VITE_OWM_BASE_URL=https://example.test/data/2.5` at build time,
|
||||
When `getWeatherData(lat, lon)` is invoked,
|
||||
Then the outgoing `fetch` URL starts with `https://example.test/data/2.5/weather?`.
|
||||
|
||||
**AC-3: Fail-soft when key is unset**
|
||||
Given `VITE_OWM_API_KEY` is unset at build time,
|
||||
When `getWeatherData(lat, lon)` is invoked,
|
||||
Then no `fetch` is made and the function returns `null`.
|
||||
|
||||
**AC-4: Default base URL when only the URL var is unset**
|
||||
Given `VITE_OWM_API_KEY` is set AND `VITE_OWM_BASE_URL` is unset at build time,
|
||||
When `getWeatherData(lat, lon)` is invoked,
|
||||
Then the outgoing URL falls back to `https://api.openweathermap.org/data/2.5/weather?...`.
|
||||
|
||||
**AC-5: Source-scan static check**
|
||||
Given the static-only profile runs (`scripts/run-tests.sh --static-only`),
|
||||
When the new `owm_key_in_source` check executes,
|
||||
Then a fresh introduction of the literal `335799082893fad97fa36118b131f919` anywhere under `src/` or `mission-planner/` (excluding test files) FAILS the build; the migrated codebase passes.
|
||||
|
||||
**AC-6: Type declarations**
|
||||
Given a TypeScript build of `mission-planner/`,
|
||||
Then `ImportMetaEnv` includes `VITE_OWM_API_KEY?: string` and `VITE_OWM_BASE_URL?: string`.
|
||||
|
||||
**AC-7: Key revocation (deliverable)**
|
||||
The previously-committed key `335799082893fad97fa36118b131f919` is revoked at the OpenWeatherMap dashboard. Closure of this AC is recorded in the implementation report by including a screenshot or a dashboard URL showing the key disabled — to keep the AC verifiable without re-exposing the new key.
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
**Security**
|
||||
- The new key MUST never be committed; it lives only in `.env.local` (gitignored) for dev and in CI secrets for builds.
|
||||
- The old key MUST be revoked at the OWM dashboard before this task is marked Done.
|
||||
|
||||
**Compatibility**
|
||||
- `WeatherService.getWeatherData(lat, lon)` signature is preserved; callers see no behavioral change beyond `null` returned when the key is unset.
|
||||
|
||||
## Unit Tests
|
||||
|
||||
| AC Ref | What to Test | Required Outcome |
|
||||
|--------|------------------------------------------------------|---------------------------------------------------|
|
||||
| AC-1 | env mocked with key only | URL contains `appid=<key>&units=metric` |
|
||||
| AC-2 | env mocked with custom base URL | URL prefix matches the env-set base |
|
||||
| AC-3 | env mocked with key unset | `getWeatherData` returns `null`; no `fetch` call |
|
||||
| AC-4 | env mocked with key set, base URL unset | URL prefix = default production OWM base |
|
||||
| AC-6 | TS compile against `ImportMetaEnv` | Compiles; new keys present, no `any` widening |
|
||||
|
||||
## Blackbox Tests
|
||||
|
||||
| AC Ref | Initial Data/Conditions | What to Test | Expected Behavior | NFR References |
|
||||
|--------|------------------------------------------------|--------------------------------------------------------------|------------------------------------|----------------|
|
||||
| AC-5 | Static-only profile run | `check-banned-deps.mjs --kind=owm_key_in_source` | Pass on clean tree; fail on regression | NFT-SEC-09 |
|
||||
|
||||
## Constraints
|
||||
|
||||
- Do NOT change `WeatherService.getWeatherData`'s public signature.
|
||||
- Do NOT add a new dependency to `mission-planner/package.json`. The change is configuration-only.
|
||||
- The new banned-deps kind MUST follow the same JSON shape as existing entries in `tests/security/banned-deps.json` so `check-banned-deps.mjs` doesn't need branching logic.
|
||||
|
||||
## Risks & Mitigation
|
||||
|
||||
**Risk 1: New key leakage during rollout**
|
||||
- *Risk*: The replacement OWM key could be committed by mistake when devs set it up locally.
|
||||
- *Mitigation*: The new `owm_key_in_source` static check catches any literal value in source. Pair with a pre-commit hook (out of scope; flagged as a future improvement) for local enforcement.
|
||||
|
||||
**Risk 2: Mission-planner has no test runner today**
|
||||
- *Risk*: `mission-planner/` doesn't have Vitest/Jest wired (module-layout.md note: tests TBD). The unit-test ACs above need a minimal test harness.
|
||||
- *Mitigation*: Either (a) wire a minimal Vitest setup for `mission-planner/` (treat as a small in-task investment), or (b) move the unit-test ACs into integration coverage on the main SPA's harness if `mission-planner` shares a build context. Choose at implementation time; the simpler option wins.
|
||||
|
||||
**Risk 3: Revocation timing**
|
||||
- *Risk*: If the old key is revoked before this code lands, every mission-planner build using the old key (dev/CI) breaks.
|
||||
- *Mitigation*: Rotate the key AT THE SAME TIME the code change is merged: PR description includes the revocation timing; dev `.env.local` files updated in lock-step with merge.
|
||||
|
||||
## Contract
|
||||
|
||||
(Omitted — this task does not produce or consume an internal suite contract; OpenWeatherMap is an external 3rd-party API and its shape is owned by them.)
|
||||
|
||||
### Document Dependencies
|
||||
|
||||
- `_docs/02_tasks/done/AZ-448_refactor_owm_api_key.md` — main-SPA pattern this task mirrors.
|
||||
- `_docs/02_tasks/done/AZ-449_refactor_owm_base_url.md` — same pattern for the base URL.
|
||||
- `_docs/02_tasks/done/AZ-482_test_secrets_and_banned_libs.md` — the static-check scaffolding this task extends.
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
## Current Step
|
||||
flow: existing-code
|
||||
step: 9
|
||||
name: New Task
|
||||
status: not_started
|
||||
step: 10
|
||||
name: Implement
|
||||
status: in_progress
|
||||
sub_step:
|
||||
phase: 0
|
||||
name: awaiting-invocation
|
||||
@@ -18,3 +18,8 @@ tracker: jira
|
||||
(+ `structure_2026-05-12.md`). Lessons appended to `_docs/LESSONS.md`.
|
||||
Steps 14, 15, 16 SKIPPED (character-identical structural refactor); no
|
||||
auth/wire/perf/deploy-relevant surface changed.
|
||||
- Cycle 2 Step 9 (New Task) COMPLETED. Epic AZ-497 created; Stories AZ-498
|
||||
(tile swap, 5 pts) + AZ-499 (mission-planner OWM hardening, 2 pts) under
|
||||
it. Contract drafted at `_docs/02_document/contracts/satellite-provider/
|
||||
tiles.md` (v1.0.0). Cross-workspace prereq for AZ-498: satellite-provider
|
||||
cookie-auth ticket (user-filed, not yet linked).
|
||||
|
||||
Reference in New Issue
Block a user