anthodev/lapin-futepublic Fork
⑂ develop
Commits
⬇ Clone ▾
git clone https://git.rickub.com/anthodev/lapin-fute.git
git clone ssh://git@rickub.com/anthodev/lapin-fute.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

[feature] Normalize live PRIM departures #4

Closed@github-anthodev opened this issue Aug 29, 2026
  • @github-anthodev opened this issue · Aug 29, 2026

    🔢 Impact Version (SemVer)

    MINOR


    🌿 Suggested Branch

    feature/normalize-live-departures/backend/4


    📝 Description

    Turn PRIM SIRI Lite responses into the shared result/error contracts while transiently relaying each user's personal key only on a 60-second cache miss. Validate/reduce upstream data, preserve time/status semantics, share non-secret fresh results, coalesce safely, expose quota failures, and instrument request/latency/byte counts without logging credentials or user configuration.


    ✅ Expected Behavior

    A valid-key miss—or a fresh public-data cache hit carrying any non-empty key—returns normalized all-mode data. The bearer is upstream credential handoff, not app identity: a hit does not validate it; 401/403 exists only on a PRIM miss. Source, malformed, quota, and failure cases map stably; concurrent misses single-flight without one caller's credential failure poisoning another event.


    📋 Acceptance Criteria

    • Resolve only service IDs produced by the catalog boundary into PRIM requests; unknown IDs return INVALID_SERVICE.
    • Require a non-empty Authorization: Bearer <personal PRIM key> before cache lookup; missing returns API_KEY_REQUIRED. Treat it as transient upstream credential, not user authentication/identity, and forward only on a cache miss as PRIM apikey to the exact HTTPS host with gzip accepted.
    • Hold the raw key only in request/upstream-call memory; redact Authorization and upstream apikey headers before framework/proxy/APM/error logging. Never persist, echo, meter, hash into a cache key, or include the key in response bodies.
    • Map an upstream PRIM 401/403 to API_KEY_INVALID—never claim validation on a cache hit—and 429 to RATE_LIMITED; capture actual quota/retry header semantics live, exposing only validated bounded retryAfterSeconds.
    • Preserve aimed and expected timestamps and calculate minutes against a clearly defined backend clock/time-zone rule.
    • Define and freeze source-normalization rules for REALTIME, SCHEDULED, and MIXED, including exactly when a multi-departure result is MIXED; preserve STALE in the shared enum but leave useful-stale age and failure-state selection to [feature] Harden failures and compatibility (https://github.com/Anthodev/lapin-fute/issues/8).
    • Normalize source status into ON_TIME, DELAYED, CANCELLED, or UNKNOWN without inventing a delay/cancellation absent from the source.
    • Define “comparable departures” and calculate nextIntervalMinutes only for consecutive departures that meet that rule.
    • Bound request/header size, upstream connection/read time, decompressed SIRI bytes, and concurrent upstream calls; reject overflow without parsing/forwarding and never send raw SIRI to client/watch.
    • Cache one binding-free public snapshot per resolved service/query for exactly 60 seconds—never request/favorite/key/user data—and preserve source time; wrap success per caller. On leader 401/403/429, fan a separately bound error to the leader and every queued event presenting that same in-flight key, clear those entries, then promote exactly one different-key event if present; otherwise terminate the flight. Keep remaining different-key waiters coalesced and never persist/hash a key for matching.
    • An expired entry makes one attempt per promoted event. A shared source failure is wrapped into a separate correctly bound ErrorResult per waiter, never cached or labeled STALE; make zero immediate retries for the same event/credential.
    • Emit credential-free counters/histograms for cache hit/miss, upstream request count/status/latency, normalized response bytes, and coalescing; cache-hit p95 backend latency must remain ≤200 ms under the recorded representative load.
    • Live queries return normalized results for one bus, one metro, and one RER or Transilien service with current coverage, preserving the source Gate.
    • Deterministic response fixtures cover bus, metro, tram, RER, and Transilien; current live smoke covers one example of each mode available in PRIM's published real-time perimeter, and records any source-declared temporary coverage absence without narrowing the implemented mode contract.
    • Recorded delayed, cancelled, scheduled-only, mixed, and malformed responses produce the specified result or error.
    • Valid concurrent callers trigger one PRIM call and receive their own bindings. Same-key-only 401/403/429 queues all settle from one failure; mixed-key queues drain failed-key waiters then promote one valid different-key leader without retrying/bursting.
    • Cache age uses original source timestamps; repeated app-open/favorite-change/Select events inside 60 seconds add zero PRIM calls and never reuse another caller's binding.
    • Record live/quota output, all-waiter settlement and mixed-key promotion traces, cache bindings/age, PRIM counts, latency/bytes, and redacted secret scans in a resolution comment.

    🔗 Affected Endpoints/Routes

    • New read-only departure route accepting one opaque serviceId and the personal key only through Authorization: Bearer.
    • Freeze exact path, 60-second cache semantics, HTTP/error mapping (401 API_KEY_REQUIRED|API_KEY_INVALID, 429 RATE_LIMITED with optional sanitized retryAfterSeconds), and JSON contracts in tests.
    • The route never accepts key in URL/query/body, returns it, forwards outside fixed PRIM HTTPS, or exposes raw SIRI. Credential length, timeout, decompressed-byte, and global upstream-concurrency bounds are repository-owned constants tested at boundaries.

    🧪 Tests to Add

    Type Coverage
    Unit tests Timestamp/minute calculation, source freshness/status, mixed/comparable interval rules, stable key/quota/source error mapping
    Schema/fixture tests Valid, delayed, cancelled, scheduled-only, mixed, partial, malformed, 401/403/429, and source-failure SIRI responses
    HTTP/security tests Handoff vs identity, hit non-validation, miss-only 401/403, header/request/timeout/decompressed-byte/concurrency bounds, exact-host forwarding/redaction, no key leakage
    Cache/concurrency tests Binding-free 60-second cache; per-caller success/error; same-key failed queue settlement; mixed-key one-at-a-time promotion; no retry/burst
    Performance/mode smoke Cache-hit p95 ≤200 ms; request/status/latency/byte metrics; deterministic bus, metro, tram, RER, Transilien fixtures and current live examples

    ⚙️ Constraints

    • Reuse the exact contracts frozen by [feature] Establish the application foundation and contracts (https://github.com/Anthodev/lapin-fute/issues/2).
    • PRIM is the only live source; every production upstream miss uses the requesting user's personal key.
    • Expected/aimed times remain distinct; omit interval without two comparable departures.
    • Fresh cache TTL is exactly 60 seconds, a product policy aligned to PRIM's documented minute-grained next-passage guidance. Useful-stale watch age belongs exclusively to [feature] Harden failures and compatibility (https://github.com/Anthodev/lapin-fute/issues/8).
    • Shared cache contains only binding-free public transport snapshots; requestId/favoriteId are applied per response. Final attribution/license verification belongs to [feature] Ship Lapin Futé version 1 (https://github.com/Anthodev/lapin-fute/issues/9).
    • The backend has no server-owned PRIM key, credential database, user account, or key-derived persistent identifier.

    🔀 Dependencies

    • Blocked by [feature] Establish the application foundation and contracts (https://github.com/Anthodev/lapin-fute/issues/2).

    ⚠️ Impact on Existing Behavior

    Introduces the production departure boundary consumed by PebbleKit JS. SIRI parsing stays backend-side; the personal key stays phone-local at rest and exists on the backend only during the authenticated relay call.


    🔍 Technical Context

    DepartureResult carries current request/favorite binding plus original source/cache timestamps. Shared cache stores only the binding-free normalized snapshot and wraps it per caller. ErrorResult has stable code and optional sanitized retryAfterSeconds, never raw quota/secret detail. Live fixtures use an operator personal test key supplied only at runtime.

    [feature] Resolve valid IDFM services (https://github.com/Anthodev/lapin-fute/issues/3) owns the production opaque-service catalog but is not a blocker: preserve the parallel branch by using recorded backend-only service-resolution fixtures here. [feature] Deliver Pebble watch consultation (https://github.com/Anthodev/lapin-fute/issues/7) validates the composed catalog→departure path after both siblings close.

    Required references

Sign in to comment.