# Migrate UAV upload capturedAt to DateTimeOffset **Task**: AZ-1126_captured_at_datetimeoffset **Name**: Migrate UavTileMetadata.capturedAt to DateTimeOffset (F-AZ810-2) **Description**: Close security carry-over F-AZ810-2 by typing `UavTileMetadata.CapturedAt` as `DateTimeOffset` instead of `DateTime`, eliminating ambiguous `DateTimeKind.Unspecified` handling on the UAV upload metadata input path. **Complexity**: 2 points **Dependencies**: AZ-810 (HARD — metadata validation layer); AZ-488 (original upload endpoint) **Component**: SatelliteProvider.Common (UavTileMetadata) + SatelliteProvider.Api (validators) + SatelliteProvider.Services.TileDownloader (quality gate + upload handler) **Tracker**: AZ-1126 **Epic**: AZ-795 ## Problem Security finding F-AZ810-2 (cycle 8, open through cycle 12) flags that `UavTileMetadata.CapturedAt` is typed `DateTime` rather than `DateTimeOffset`. `DateTime` accepts `DateTimeKind.Unspecified` values that deserialize from offset-less ISO-8601 strings, forcing manual `Kind` normalization in the upload handler and quality gate. This is a time-handling correctness gap that can skew freshness-window checks in non-UTC dev environments. ## Outcome - `capturedAt` on the UAV upload metadata input is unambiguously UTC-aware at the type level - Offset-less or `Unspecified` timestamps are rejected before persistence - F-AZ810-2 is marked resolved in the next security audit cycle - Wire compatibility preserved for clients sending ISO-8601 UTC with explicit offset (`Z` or `+00:00`) ## Scope ### Included - Change `UavTileMetadata.CapturedAt` from `DateTime` to `DateTimeOffset` - Update FluentValidation rules, quality gate, and upload handler to compare via UTC without manual `Kind` branching - Reject offset-less / ambiguous `capturedAt` values with HTTP 400 - Unit tests for the new rejection path and existing freshness-window rules - Integration test proving offset-less `capturedAt` is rejected - Patch `_docs/02_document/contracts/api/uav-tile-upload.md` 1.2.0 → 1.2.1 (change log + clarify offset requirement) ### Excluded - `TileInventoryEntry.CapturedAt` response field (remains `DateTime?` — DB read path) - `TileEntity.CapturedAt` persistence layer type - Changes to gRPC tile delivery or other non-UAV-upload surfaces - MAJOR contract version bump (wire JSON shape unchanged for compliant clients) ## Acceptance Criteria **AC-1: Type migration** Given the UAV upload metadata DTO When deserialized from JSON Then `CapturedAt` is `DateTimeOffset` and freshness comparisons use UTC without manual `DateTimeKind` normalization **AC-2: Reject ambiguous timestamps** Given a UAV upload batch with `capturedAt` lacking an explicit UTC offset (offset-less ISO string) When POST `/api/satellite/upload` Then HTTP 400 with a validation or deserialization error referencing `capturedAt` **AC-3: Backward-compatible UTC clients** Given a UAV upload batch with `capturedAt` as ISO-8601 UTC (`...Z` or `...+00:00`) When POST `/api/satellite/upload` with otherwise valid payload Then HTTP 200 (or the same non-timestamp rejection as before timestamp validation) **AC-4: Contract patch** Given the implementation is complete When `uav-tile-upload.md` is reviewed Then version is 1.2.1 with a change-log entry documenting the offset requirement and F-AZ810-2 closure ## Non-Functional Requirements **Compatibility** - Compliant clients already sending `Z`-suffixed timestamps must not break **Security** - Closes F-AZ810-2 (Low / informational time-handling finding) ## Unit Tests | AC Ref | What to Test | Required Outcome | |--------|-------------|-----------------| | AC-1 | `UavTileMetadataValidator` freshness window with `DateTimeOffset` | Future/too-old rules still fire | | AC-2 | Deserializer or validator with offset-less `capturedAt` | Rejected | | AC-1 | `UavTileQualityGate` captured-at rules | Still accept/reject correctly | ## Blackbox Tests | AC Ref | Initial Data/Conditions | What to Test | Expected Behavior | NFR References | |--------|------------------------|-------------|-------------------|----------------| | AC-2 | Valid JPEG + metadata with `capturedAt: "2026-06-26T12:00:00"` (no offset) | POST `/api/satellite/upload` | HTTP 400 mentioning `capturedAt` | — | | AC-3 | Valid JPEG + metadata with `capturedAt` as `DateTime.UtcNow.ToString("o")` | POST `/api/satellite/upload` | HTTP 200 (happy path unchanged) | Compatibility | ## Constraints - Must remain a child of epic AZ-795 strict-validation theme - No breaking wire change for offset-aware clients ## Risks & Mitigation **Risk 1: Client sends offset-less timestamps** - *Risk*: Legitimate clients using `"2026-06-26T12:00:00"` without `Z` start failing - *Mitigation*: Contract patch documents requirement; rejection is intentional per F-AZ810-2 ## Contract This task patches the producer contract at `_docs/02_document/contracts/api/uav-tile-upload.md` (1.2.0 → 1.2.1). Consumers: `gps-denied-onboard`, mission planner UI, any UAV upload client. ### Document Dependencies - `_docs/02_document/contracts/api/uav-tile-upload.md` v1.2.0 (patch to 1.2.1) - `_docs/02_document/contracts/api/error-shape.md` v1.0.0 (unchanged)