Skip to content
Open
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
27 changes: 23 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,38 +154,57 @@ The server updates its in-memory state with the latest position and persists the

**`POST /api/v1/locations` validation and error contract**

The ingest endpoint performs strict request validation before writing data:
The ingest endpoint requires a driver JWT (`Authorization: Bearer <token>`)
and performs strict request validation before writing data:
Comment on lines +157 to +158

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 12 \
  'handlePostLocation|requireAuth|adminMiddleware|driver|role|claims' \
  --glob '*.go' .

Repository: OneBusAway/vehicle-positions

Length of output: 50385


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- route wiring ---'
rg -n -C 8 'func newMux|handlePostLocation|requireAuth|requireAdmin|/api/v1/locations' --glob '*.go' .

printf '%s\n' '--- authentication implementation ---'
rg -n -C 14 'func requireAuth|func requireAdmin|generateJWT|claimsKey' --glob '*.go' .

printf '%s\n' '--- README context ---'
sed -n '150,165p' README.md

Repository: OneBusAway/vehicle-positions

Length of output: 50384


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

files = [p for p in Path(".").glob("*.go")]
text = "\n".join(p.read_text() for p in files)

route = re.search(
    r'(?s)(?:Handle|Methods|Path|Route|Get|Post)\([^;]{0,500}/api/v1/locations[^;]{0,500}',
    text,
)
print("location route references:")
for i, line in enumerate(text.splitlines(), 1):
    if "/api/v1/locations" in line or "handlePostLocation" in line:
        print(f"{i}: {line}")
print("\nrole checks near location route:")
lines = text.splitlines()
for i, line in enumerate(lines):
    if "/api/v1/locations" in line:
        lo, hi = max(0, i - 8), min(len(lines), i + 4)
        print("\n".join(f"{j+1}: {lines[j]}" for j in range(lo, hi)))
PY

Repository: OneBusAway/vehicle-positions

Length of output: 12869


Enforce the driver role or update the documentation. POST /api/v1/locations uses authentication only. handlePostLocation reads sub but does not check role, so any valid JWT with a non-empty sub can submit locations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 157 - 158, Update handlePostLocation to validate that
the authenticated JWT includes the required driver role before accepting or
writing location data, while preserving the existing sub validation;
alternatively, revise the README ingest-endpoint documentation to accurately
state that authentication alone is sufficient. Ensure the implementation and
documentation consistently describe the enforced access requirements.


- `Content-Type` must be `application/json` (charset parameters are allowed).
- The request body must contain exactly one JSON object.
- Unknown JSON fields are rejected.
- Standard payload validation still applies (`vehicle_id`, coordinates, timestamp).
- Reports are rate limited to one per five seconds per driver.

Response codes:

- `201 Created` — location accepted and persisted.
- `400 Bad Request` — invalid JSON or payload validation failure.
- `401 Unauthorized` — missing, malformed, or expired bearer token.
- `415 Unsupported Media Type` — non-JSON `Content-Type`.
- `429 Too Many Requests` — driver exceeded the ingest rate limit.
Comment on lines 160 to +172

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 12 \
  'handlePostLocation|VehicleRateLimiter|DisallowUnknownFields|StatusBadRequest|StatusUnsupportedMediaType|StatusTooManyRequests|json\.Decoder' \
  --glob '*.go' .

Repository: OneBusAway/vehicle-positions

Length of output: 50385


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- matching files ---'
rg -l --glob '*.go' 'handlePostLocation|VehicleRateLimiter' .

printf '%s\n' '--- matching declarations and uses ---'
rg -n --glob '*.go' '^(type|func) .*?(VehicleRateLimiter|handlePostLocation)|VehicleRateLimiter|handlePostLocation' .

Repository: OneBusAway/vehicle-positions

Length of output: 3555


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

ast-grep outline handlers.go
ast-grep outline ratelimit.go
ast-grep outline handlers_test.go

Repository: OneBusAway/vehicle-positions

Length of output: 4901


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- handlers.go ---'
sed -n '1,140p' handlers.go

printf '%s\n' '--- ratelimit.go ---'
sed -n '1,95p' ratelimit.go

printf '%s\n' '--- README.md ---'
sed -n '150,180p' README.md

printf '%s\n' '--- validation and response tests ---'
sed -n '180,290p' handlers_test.go
sed -n '470,610p' handlers_test.go

printf '%s\n' '--- rate-limit and identity tests ---'
sed -n '606,755p' handlers_test.go

Repository: OneBusAway/vehicle-positions

Length of output: 24803


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- authentication and route wiring ---'
rg -n -C 18 'func authMiddleware|func requireAuth|StatusUnauthorized|Authorization|jwt.Parse|POST /api/v1/locations' --glob '*.go' .

printf '%s\n' '--- complete README contract and examples ---'
sed -n '160,215p' README.md

printf '%s\n' '--- static contract verifier ---'
python3 - <<'PY'
from pathlib import Path

handlers = Path("handlers.go").read_text()
ratelimit = Path("ratelimit.go").read_text()

checks = {
    "JSON media type with parameters": 'mime.ParseMediaType(contentType)' in handlers,
    "unknown fields rejected": 'decoder.DisallowUnknownFields()' in handlers,
    "trailing JSON rejected": 'decoder.Decode(new(json.RawMessage))' in handlers,
    "payload validation before save": 'if err := loc.validate(); err != nil' in handlers and
        handlers.index('if err := loc.validate(); err != nil') < handlers.index('store.SaveLocation'),
    "rate-limit key is JWT subject": 'rl.Allow(loc.DriverID)' in handlers and 'loc.DriverID = sub' in handlers,
    "429 mapping": 'http.StatusTooManyRequests' in handlers,
    "save precedes tracker update": handlers.index('store.SaveLocation') < handlers.index('tracker.Update'),
    "capacity bypass exists": 'allowing untracked key' in ratelimit and
        'return true' in ratelimit[ratelimit.index('if len(vrl.limiters) >= maxTrackedRates'):],
}
for name, result in checks.items():
    print(f"{name}: {'PASS' if result else 'FAIL'}")
PY

Repository: OneBusAway/vehicle-positions

Length of output: 48530


Enforce the rate limit when the limiter reaches capacity.

VehicleRateLimiter.Allow permits every new driver key after 10,000 keys are tracked. A new driver can therefore bypass the documented five-second limit. Reject or rate-limit untracked keys instead of returning true.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 160 - 172, Update VehicleRateLimiter.Allow so that
when the tracked-driver capacity reaches 10,000 keys, newly untracked driver
keys are rejected or rate-limited rather than returning true. Preserve the
existing five-second allowance for tracked keys and normal behavior while
capacity remains available.

- `500 Internal Server Error` — the location could not be persisted. The
in-memory tracker is left untouched, so a failed write never reaches the
GTFS-RT feed.

Examples:

```bash
# Valid request
curl -i -X POST http://localhost:8080/api/v1/locations \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"vehicle_id":"bus-1","trip_id":"route-5","latitude":-1.29,"longitude":36.82,"timestamp":1752566400}'
-d '{"vehicle_id":"bus-1","trip_id":"route-5","latitude":-1.29,"longitude":36.82,"timestamp":'"$(date +%s)"'}'

# Missing bearer token -> 401
curl -i -X POST http://localhost:8080/api/v1/locations \
-H "Content-Type: application/json" \
-d '{"vehicle_id":"bus-1","latitude":-1.29,"longitude":36.82,"timestamp":'"$(date +%s)"'}'

# Invalid content type -> 415
curl -i -X POST http://localhost:8080/api/v1/locations \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: text/plain" \
-d '{"vehicle_id":"bus-1","latitude":-1.29,"longitude":36.82,"timestamp":1752566400}'
-d '{"vehicle_id":"bus-1","latitude":-1.29,"longitude":36.82,"timestamp":'"$(date +%s)"'}'

# Trailing JSON value -> 400
curl -i -X POST http://localhost:8080/api/v1/locations \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"vehicle_id":"bus-1","latitude":-1.29,"longitude":36.82,"timestamp":1752566400}{"extra":1}'
-d '{"vehicle_id":"bus-1","latitude":-1.29,"longitude":36.82,"timestamp":'"$(date +%s)"'}{"extra":1}'
```

`$TOKEN` is the JWT returned by `POST /api/v1/auth/login`. The examples use
`$(date +%s)` rather than a fixed timestamp because reports more than five
minutes from server time are rejected with `400`.

**Technology Stack:**

- **Language:** Go (aligns with Maglev and OTSF’s server-side direction)
Expand Down