Skip to content

WIP: feat: Fleet Engine (ODRD) REST client, config and JWT auth#1381

Open
prakharritik wants to merge 2 commits into
mainfrom
claude/fleet-engine-odrd-sdk
Open

WIP: feat: Fleet Engine (ODRD) REST client, config and JWT auth#1381
prakharritik wants to merge 2 commits into
mainfrom
claude/fleet-engine-odrd-sdk

Conversation

@prakharritik

@prakharritik prakharritik commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Type of Change

  • New feature

Description

Adds a new Kernel.External.FleetEngine.* module tree — the shared foundation for Google Fleet Engine (On-demand Rides & Deliveries / journey sharing), consumed by the driver-app (BPP) trip mirroring and the rider/driver auth-token endpoints.

  • Types.hsTripStatus / TripType enums (JSON spelled exactly as the Fleet Engine REST enums), LatLng, TerminalLocation, a partial Trip resource (Nothing fields omitted so a PATCH never clobbers server state), and mkCreateTripBody.
  • Config.hsFleetEngineCfg (provider/project id, optional base URL, encrypted service-account JSON, per-token-type TTLs), modelled on Kernel.External.Maps.Google.Config.GoogleCfg.
  • Auth.hs — mints short-lived self-signed Fleet Engine JWTs scoped to a trip (consumer), a vehicle (driver), or unrestricted (server), reusing the existing Google service-account signing in Kernel.Utils.JWT.
  • Client.hs — Servant client (createTrip, updateTrip, updateTripStatus, assignVehicleAndStart) modelled on Kernel.External.Maps.Google.MobilityBilling. createTrip is keyed on the BPP ride id, so a re-issue returns ALREADY_EXISTS and is treated as an idempotent success.
  • Kernel.Utils.JWT — adds createSignedJWTWithClaims (a self-signed JWT with caller-supplied audience/TTL/claims), used by Auth.hs.

Reuses the existing Google SA primitives (ServiceAccount, Web.JWT signing) and the callAPI Servant pattern — no new heavy dependencies. There are no call sites in this repo; the driver-app PR wires it in.

Additional Changes

  • This PR modifies the database schema (database migration added)
  • This PR modifies dhall configs/environment variables

Motivation and Context

Rider-side journey-sharing ETAs (driver→pickup and vehicle→destination) require the ride to exist as a Fleet Engine trip. Fleet Engine has no "bring-your-own-data" mode, so the backend must mirror trips and mint scoped SDK tokens. This is the shared client/auth layer those services build on.

How did you test it?

Kernel.External.FleetEngine is a self-contained library addition that mirrors the existing Google Maps client/auth patterns. It builds with cabal build in the Nix dev shell (not runnable in the authoring sandbox, which has no nix/cabal). Suggested follow-up: HUnit tests for JWT claim/expiry shape and TripStatus JSON, plus a staging smoke test of createTrip/updateTripStatus (asserting idempotent ALREADY_EXISTS).

Checklist

  • I formatted the code and addressed linter errors ./dev/format-all-files.sh
  • I reviewed submitted code
  • I added unit tests for my changes where possible
  • I added a CHANGELOG entry if applicable

🤖 Generated with Claude Code


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Added support for Google Fleet Engine trips, including create/update actions plus convenience helpers for updating trip status and assigning vehicles to start service.
    • Introduced Fleet Engine configuration for provider settings, service account, base URL, and token TTL overrides.
    • Added Fleet Engine trip data types and JSON handling for create/update payloads.
    • Added authentication support to mint short-lived self-signed JWTs for Fleet Engine, including scoped authorization claims.

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@prakharritik, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 26 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0f6a3fde-a4f4-426f-9889-91356af494ce

📥 Commits

Reviewing files that changed from the base of the PR and between 45b76d4 and 800c68c.

📒 Files selected for processing (6)
  • lib/mobility-core/mobility-core.cabal
  • lib/mobility-core/src/Kernel/External/FleetEngine/Auth.hs
  • lib/mobility-core/src/Kernel/External/FleetEngine/Client.hs
  • lib/mobility-core/src/Kernel/External/FleetEngine/Config.hs
  • lib/mobility-core/src/Kernel/External/FleetEngine/Types.hs
  • lib/mobility-core/src/Kernel/Utils/JWT.hs

Walkthrough

Adds Fleet Engine support in mobility-core with new auth, trip type, client, and config modules, plus a reusable JWT signing primitive. The library cabal file now exposes the new Fleet Engine modules.

Changes

Fleet Engine Integration

Layer / File(s) Summary
JWT signing primitive
lib/mobility-core/src/Kernel/Utils/JWT.hs
Adds createSignedJWTWithClaims to mint self-signed RS256 JWTs with caller-supplied audience, TTL, and extra unregistered claims.
Domain types and configuration
lib/mobility-core/src/Kernel/External/FleetEngine/Types.hs, lib/mobility-core/src/Kernel/External/FleetEngine/Config.hs
Defines Fleet Engine trip enums, coordinates, trip records, JSON helpers, create-trip body construction, and per-merchant Fleet Engine configuration with token TTL defaults.
Fleet Engine JWT auth module
lib/mobility-core/src/Kernel/External/FleetEngine/Auth.hs
Defines TokenScope, parses service-account JSON, and mints Fleet Engine JWTs with scope-derived authorization claims.
Fleet Engine Servant client
lib/mobility-core/src/Kernel/External/FleetEngine/Client.hs
Defines CreateTrip and UpdateTrip APIs, implements trip creation/update calls, and adds convenience wrappers for status and vehicle assignment updates.
Cabal module registration
lib/mobility-core/mobility-core.cabal
Registers the four new Fleet Engine modules in exposed-modules.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested reviewers: khuzema786, bytedex, Vignesh-772

Poem

A rabbit hopped through code so neat,
With JWT hops and trip types sweet.
New Fleet Engine paths now glow at night,
With config, client, and auth just right.
🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: adding Fleet Engine REST client, config, and JWT auth support.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/fleet-engine-odrd-sdk

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with 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.

Inline comments:
In `@lib/mobility-core/src/Kernel/External/FleetEngine/Client.hs`:
- Around line 77-118: Both Fleet Engine write helpers currently hide failures by
returning m () after logging, so callers cannot react to remote write errors.
Update createTrip and updateTrip in Client.hs to propagate non-success responses
from callAPI by throwing/returning an error in the MonadFlow flow instead of
just logging; keep the ALREADY_EXISTS branch in createTrip as the only
idempotent success path, and ensure the change is applied where result is
pattern-matched in these functions.
- Around line 92-97: The idempotency check in FleetEngine createTrip handling is
too brittle because it relies on matching "ALREADY_EXISTS" in show err. Update
the Left err branch in Client.hs to inspect the structured
FailureResponse/response body from the FleetEngine error instead of stringifying
the exception, and use that parsed status/body to decide whether to log the
idempotent no-op or fall through to logError. Keep the existing logInfo/logError
paths, but key the duplicate-create detection off the response payload rather
than the rendered exception text.

In `@lib/mobility-core/src/Kernel/External/FleetEngine/Config.hs`:
- Around line 30-37: The FleetEngine Config currently uses a single
serviceAccountJson for both SDK token minting and server-to-server
authentication, which mixes two different trust domains. Split the signer
configuration in the FleetEngine config and related token-minting code so
consumer/driver JWTs continue to use the shared SDK signer while
server-to-server tokens use a separate backend service account or ADC path.
Update the relevant Config type and any token creation helpers that reference
serviceAccountJson to distinguish these two identities cleanly.

In `@lib/mobility-core/src/Kernel/External/FleetEngine/Types.hs`:
- Around line 86-96: Make mkCreateTripBody require a pickup point instead of
accepting it as optional, since Fleet Engine create-trip requests need
pickup_point and trip_type. Update the mkCreateTripBody signature and its use of
pickupPoint so it is always set from a provided LatLng, and remove tripStatus
from the create body because it is ignored on create. Keep the rest of the Trip
construction in emptyTrip unchanged.

In `@lib/mobility-core/src/Kernel/Utils/JWT.hs`:
- Around line 123-150: The `createSignedJWTWithClaims` function should validate
inputs before encoding the token: reject any audience that cannot be converted
by `stringOrURI` and return an error instead of proceeding with missing `aud`,
`iss`, or `sub`, and also fail fast when `ttlSeconds` is zero or negative so the
JWT is never created already expired. Update the logic around `stringOrURI`,
`audience'`, and the `exp` calculation in `createSignedJWTWithClaims` to produce
a `Left` for invalid audience or non-positive TTL, while keeping the existing
signing flow unchanged for valid inputs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d3c48b98-a10e-4a27-be58-36336b3a8445

📥 Commits

Reviewing files that changed from the base of the PR and between b4dd2e4 and 91eb0ab.

📒 Files selected for processing (6)
  • lib/mobility-core/mobility-core.cabal
  • lib/mobility-core/src/Kernel/External/FleetEngine/Auth.hs
  • lib/mobility-core/src/Kernel/External/FleetEngine/Client.hs
  • lib/mobility-core/src/Kernel/External/FleetEngine/Config.hs
  • lib/mobility-core/src/Kernel/External/FleetEngine/Types.hs
  • lib/mobility-core/src/Kernel/Utils/JWT.hs

Comment on lines +77 to +118
createTrip ::
(CoreMetrics m, MonadFlow m, MonadReader r m, HasRequestId r) =>
BaseUrl ->
Text -> -- providerId
Text -> -- token (server JWT)
Text -> -- tripId
Trip ->
m ()
createTrip baseUrl providerId token tripId trip = do
result <-
callAPI
baseUrl
(createTripClient providerId tripId (bearer token) trip)
"fleetEngineCreateTrip"
(Proxy :: Proxy CreateTripAPI)
case result of
Right _ -> logInfo $ "FleetEngine: created trip " <> tripId
Left err
| "ALREADY_EXISTS" `T.isInfixOf` show err ->
logInfo $ "FleetEngine: trip already exists (idempotent no-op) " <> tripId
| otherwise -> logError $ "FleetEngine: createTrip failed for " <> tripId <> ": " <> show err

-- | PATCH a trip with the given field mask and body.
updateTrip ::
(CoreMetrics m, MonadFlow m, MonadReader r m, HasRequestId r) =>
BaseUrl ->
Text -> -- providerId
Text -> -- token (server JWT)
Text -> -- tripId
Text -> -- updateMask (comma-separated field paths)
Trip ->
m ()
updateTrip baseUrl providerId token tripId updateMask trip = do
result <-
callAPI
baseUrl
(updateTripClient providerId tripId updateMask (bearer token) trip)
"fleetEngineUpdateTrip"
(Proxy :: Proxy UpdateTripAPI)
case result of
Right _ -> logInfo $ "FleetEngine: updated trip " <> tripId <> " [" <> updateMask <> "]"
Left err -> logError $ "FleetEngine: updateTrip failed for " <> tripId <> ": " <> show err

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 | 🟠 Major | 🏗️ Heavy lift

Propagate Fleet Engine write failures instead of only logging them.

Both write helpers return m () and swallow every non-success response after logging. That makes upstream code proceed as if the remote trip state was updated, which can permanently desync local ride state from Fleet Engine. Fix this at the root by returning/throwing an error from createTrip and updateTrip, and keep the ALREADY_EXISTS branch as the only idempotent success case.

🤖 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 `@lib/mobility-core/src/Kernel/External/FleetEngine/Client.hs` around lines 77
- 118, Both Fleet Engine write helpers currently hide failures by returning m ()
after logging, so callers cannot react to remote write errors. Update createTrip
and updateTrip in Client.hs to propagate non-success responses from callAPI by
throwing/returning an error in the MonadFlow flow instead of just logging; keep
the ALREADY_EXISTS branch in createTrip as the only idempotent success path, and
ensure the change is applied where result is pattern-matched in these functions.

Comment on lines +92 to +97
case result of
Right _ -> logInfo $ "FleetEngine: created trip " <> tripId
Left err
| "ALREADY_EXISTS" `T.isInfixOf` show err ->
logInfo $ "FleetEngine: trip already exists (idempotent no-op) " <> tripId
| otherwise -> logError $ "FleetEngine: createTrip failed for " <> tripId <> ": " <> show err

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the actual error type returned by callAPI and existing structured handling patterns.
fd 'Common.hs' lib/mobility-core/src | xargs -r rg -n -C3 '\bcallAPI\b|FailureResponse|ClientError|ALREADY_EXISTS'
fd 'Client.hs' lib/mobility-core/src/Kernel/External | xargs -r rg -n -C3 'show err|ALREADY_EXISTS|FailureResponse'

Repository: nammayatri/shared-kernel

Length of output: 3618


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate callAPI definition and error type handling.
fd -a 'Common.hs' lib/mobility-core/src
fd -a 'Client.hs' lib/mobility-core/src/Kernel/External/FleetEngine
fd -a 'Common.hs' lib/mobility-core/src | xargs -r sed -n '1,260p'
printf '\n==== FleetEngine Client slice ====\n'
sed -n '1,180p' lib/mobility-core/src/Kernel/External/FleetEngine/Client.hs

printf '\n==== Search for error type definitions/usages ====\n'
rg -n -C3 'data .*ClientError|type .*ClientError|FailureResponse|ServantError|HttpException|callAPI\s*::|callAPI\s+\w+' lib/mobility-core/src

Repository: nammayatri/shared-kernel

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate callAPI definition and error type handling.
fd -a 'Common.hs' lib/mobility-core/src
fd -a 'Client.hs' lib/mobility-core/src/Kernel/External/FleetEngine

printf '\n==== Common.hs ====\n'
sed -n '1,260p' lib/mobility-core/src/Common.hs

printf '\n==== FleetEngine Client slice ====\n'
sed -n '1,180p' lib/mobility-core/src/Kernel/External/FleetEngine/Client.hs

printf '\n==== Search for error type definitions/usages ====\n'
rg -n -C3 'data .*ClientError|type .*ClientError|FailureResponse|ServantError|HttpException|callAPI\s*::|callAPI\s+\w+' lib/mobility-core/src

Repository: nammayatri/shared-kernel

Length of output: 728


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '==== callAPI implementation ====\n'
sed -n '1,280p' lib/mobility-core/src/Kernel/Utils/Servant/Client.hs

printf '\n==== CallAPIError helper ====\n'
sed -n '1,220p' lib/mobility-core/src/Kernel/Types/Error/BaseError/HTTPError/CallAPIError.hs

printf '\n==== ALREADY_EXISTS search ====\n'
rg -n -C4 'ALREADY_EXISTS|already exists|FailureResponse _ \(Response' lib/mobility-core/src/Kernel/External lib/mobility-core/src/Kernel/Types lib/mobility-core/src/Kernel/Utils

Repository: nammayatri/shared-kernel

Length of output: 25771


Match Fleet Engine duplicate errors structurally
Use FailureResponse/response body for the idempotency check instead of show err; the current substring match is brittle and can flip duplicate creates into failures if the rendered exception text changes.

🤖 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 `@lib/mobility-core/src/Kernel/External/FleetEngine/Client.hs` around lines 92
- 97, The idempotency check in FleetEngine createTrip handling is too brittle
because it relies on matching "ALREADY_EXISTS" in show err. Update the Left err
branch in Client.hs to inspect the structured FailureResponse/response body from
the FleetEngine error instead of stringifying the exception, and use that parsed
status/body to decide whether to log the idempotent no-op or fall through to
logError. Keep the existing logInfo/logError paths, but key the duplicate-create
detection off the response payload rather than the rendered exception text.

Comment thread lib/mobility-core/src/Kernel/External/FleetEngine/Config.hs
Comment on lines +86 to +96
-- | Build the CreateTrip body. Fleet Engine requires @tripType@; pickup/dropoff
-- are optional but improve ETA quality.
mkCreateTripBody :: TripType -> Maybe LatLng -> Maybe LatLng -> Maybe Int -> Trip
mkCreateTripBody tType mbPickup mbDropoff mbPassengers =
emptyTrip
{ tripType = Just tType,
tripStatus = Just NEW,
pickupPoint = TerminalLocation <$> mbPickup,
dropoffPoint = TerminalLocation <$> mbDropoff,
numberOfPassengers = mbPassengers
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== File outline ==\n'
ast-grep outline lib/mobility-core/src/Kernel/External/FleetEngine/Types.hs --view expanded || true

printf '\n== Relevant snippet ==\n'
sed -n '1,180p' lib/mobility-core/src/Kernel/External/FleetEngine/Types.hs | cat -n

printf '\n== mkCreateTripBody usages ==\n'
rg -n "mkCreateTripBody" -S .

printf '\n== Trip / pickupPoint definitions ==\n'
rg -n "data Trip|pickupPoint|tripStatus|tripType|newtype Trip|type Trip" lib/mobility-core/src -S

Repository: nammayatri/shared-kernel

Length of output: 6240


🌐 Web query:

Fleet Engine CreateTrip trip_type pickup_point required tripStatus create request docs

💡 Result:

To create a trip in Fleet Engine using the CreateTrip request, you must provide a parent string (the project ID in the format providers/{provider}) and a unique trip_id [1][2]. Within the Trip object included in the request, the following fields are strictly required: 1. trip_type: Specifies the type of trip (e.g., EXCLUSIVE or SHARED) [1][3][4]. 2. pickup_point: Defines the point of origin for the trip [1][3][4]. Regarding the tripStatus field: it is not required in the creation request because all new trips are automatically assigned a trip_status of NEW by the Fleet Engine system [3][4]. Any value you provide for trip_status during the creation request is ignored [3][4]. Other optional fields that can be provided include dropoff_point, number_of_passengers, vehicle_id, and intermediate_destinations [3][4].

Citations:


Make pickupPoint required in mkCreateTripBody. Fleet Engine create trips require both trip_type and pickup_point; tripStatus is ignored on create and can be omitted.

🤖 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 `@lib/mobility-core/src/Kernel/External/FleetEngine/Types.hs` around lines 86 -
96, Make mkCreateTripBody require a pickup point instead of accepting it as
optional, since Fleet Engine create-trip requests need pickup_point and
trip_type. Update the mkCreateTripBody signature and its use of pickupPoint so
it is always set from a provided LatLng, and remove tripStatus from the create
body because it is ignored on create. Keep the rest of the Trip construction in
emptyTrip unchanged.

Comment on lines +123 to +150
createSignedJWTWithClaims sa audience ttlSeconds additionalClaims = do
let issuer = stringOrURI . saClientEmail $ sa
let subject = stringOrURI . saClientEmail $ sa
let audience' = Left <$> stringOrURI audience
let unregisteredClaims = ClaimsMap $ Map.fromList additionalClaims
let jwtHeader =
JOSEHeader
{ typ = Just "JWT",
cty = Nothing,
alg = Just RS256,
kid = Just $ saPrivateKeyId sa
}
case readRsaSecret . C8.pack $ saPrivateKey sa of
Nothing -> pure $ Left "Bad RSA key!"
Just pkey -> do
let key = EncodeRSAPrivateKey pkey
iat <- numericDate <$> getPOSIXTime
exp <- numericDate . (+ fromInteger ttlSeconds) <$> getPOSIXTime
let claims =
mempty
{ exp = exp,
iat = iat,
iss = issuer,
sub = subject,
aud = audience',
unregisteredClaims = unregisteredClaims
}
pure $ Right (encodeSigned key jwtHeader claims)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Reject invalid audiences and non-positive TTLs before signing.

stringOrURI can fail silently into missing iss/sub/aud, and ttlSeconds <= 0 returns a Right token that is already expired. Fleet Engine JWT guidance expects an expiry-bearing bearer token, so fail fast here instead. (developers.google.com)

Suggested fix
 createSignedJWTWithClaims sa audience ttlSeconds additionalClaims = do
-  let issuer = stringOrURI . saClientEmail $ sa
-  let subject = stringOrURI . saClientEmail $ sa
-  let audience' = Left <$> stringOrURI audience
-  let unregisteredClaims = ClaimsMap $ Map.fromList additionalClaims
-  let jwtHeader =
-        JOSEHeader
-          { typ = Just "JWT",
-            cty = Nothing,
-            alg = Just RS256,
-            kid = Just $ saPrivateKeyId sa
-          }
-  case readRsaSecret . C8.pack $ saPrivateKey sa of
-    Nothing -> pure $ Left "Bad RSA key!"
-    Just pkey -> do
+  case (stringOrURI $ saClientEmail sa, stringOrURI audience) of
+    (Nothing, _) -> pure $ Left "Invalid service account client email"
+    (_, Nothing) -> pure $ Left "Invalid JWT audience"
+    (Just issuer, Just audienceUri)
+      | ttlSeconds <= 0 -> pure $ Left "JWT TTL must be positive"
+      | otherwise -> case readRsaSecret . C8.pack $ saPrivateKey sa of
+          Nothing -> pure $ Left "Bad RSA key!"
+          Just pkey -> do
+            let subject = issuer
+            let audience' = Just $ Left audienceUri
+            let unregisteredClaims = ClaimsMap $ Map.fromList additionalClaims
+            let jwtHeader =
+                  JOSEHeader
+                    { typ = Just "JWT",
+                      cty = Nothing,
+                      alg = Just RS256,
+                      kid = Just $ saPrivateKeyId sa
+                    }
       let key = EncodeRSAPrivateKey pkey
       iat <- numericDate <$> getPOSIXTime
       exp <- numericDate . (+ fromInteger ttlSeconds) <$> getPOSIXTime
       let claims =
             mempty
               { exp = exp,
                 iat = iat,
-                iss = issuer,
-                sub = subject,
+                iss = Just issuer,
+                sub = Just subject,
                 aud = audience',
                 unregisteredClaims = unregisteredClaims
               }
       pure $ Right (encodeSigned key jwtHeader claims)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
createSignedJWTWithClaims sa audience ttlSeconds additionalClaims = do
let issuer = stringOrURI . saClientEmail $ sa
let subject = stringOrURI . saClientEmail $ sa
let audience' = Left <$> stringOrURI audience
let unregisteredClaims = ClaimsMap $ Map.fromList additionalClaims
let jwtHeader =
JOSEHeader
{ typ = Just "JWT",
cty = Nothing,
alg = Just RS256,
kid = Just $ saPrivateKeyId sa
}
case readRsaSecret . C8.pack $ saPrivateKey sa of
Nothing -> pure $ Left "Bad RSA key!"
Just pkey -> do
let key = EncodeRSAPrivateKey pkey
iat <- numericDate <$> getPOSIXTime
exp <- numericDate . (+ fromInteger ttlSeconds) <$> getPOSIXTime
let claims =
mempty
{ exp = exp,
iat = iat,
iss = issuer,
sub = subject,
aud = audience',
unregisteredClaims = unregisteredClaims
}
pure $ Right (encodeSigned key jwtHeader claims)
createSignedJWTWithClaims sa audience ttlSeconds additionalClaims = do
case (stringOrURI $ saClientEmail sa, stringOrURI audience) of
(Nothing, _) -> pure $ Left "Invalid service account client email"
(_, Nothing) -> pure $ Left "Invalid JWT audience"
(Just issuer, Just audienceUri)
| ttlSeconds <= 0 -> pure $ Left "JWT TTL must be positive"
| otherwise ->
case readRsaSecret . C8.pack $ saPrivateKey sa of
Nothing -> pure $ Left "Bad RSA key!"
Just pkey -> do
let subject = issuer
let audience' = Just $ Left audienceUri
let unregisteredClaims = ClaimsMap $ Map.fromList additionalClaims
let jwtHeader =
JOSEHeader
{ typ = Just "JWT",
cty = Nothing,
alg = Just RS256,
kid = Just $ saPrivateKeyId sa
}
let key = EncodeRSAPrivateKey pkey
iat <- numericDate <$> getPOSIXTime
exp <- numericDate . (+ fromInteger ttlSeconds) <$> getPOSIXTime
let claims =
mempty
{ exp = exp,
iat = iat,
iss = Just issuer,
sub = Just subject,
aud = audience',
unregisteredClaims = unregisteredClaims
}
pure $ Right (encodeSigned key jwtHeader claims)
🤖 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 `@lib/mobility-core/src/Kernel/Utils/JWT.hs` around lines 123 - 150, The
`createSignedJWTWithClaims` function should validate inputs before encoding the
token: reject any audience that cannot be converted by `stringOrURI` and return
an error instead of proceeding with missing `aud`, `iss`, or `sub`, and also
fail fast when `ttlSeconds` is zero or negative so the JWT is never created
already expired. Update the logic around `stringOrURI`, `audience'`, and the
`exp` calculation in `createSignedJWTWithClaims` to produce a `Left` for invalid
audience or non-positive TTL, while keeping the existing signing flow unchanged
for valid inputs.

@prakharritik prakharritik changed the title feat: Fleet Engine (ODRD) REST client, config and JWT auth WIP: feat: Fleet Engine (ODRD) REST client, config and JWT auth Jun 29, 2026
@prakharritik prakharritik force-pushed the claude/fleet-engine-odrd-sdk branch from 45b76d4 to b15d415 Compare July 1, 2026 11:33
claude added 2 commits July 1, 2026 11:48
Add Kernel.External.FleetEngine.{Types,Config,Client,Auth} providing a
Servant client for the Google Fleet Engine (On-demand Rides & Deliveries)
trip API (createTrip / updateTrip / updateTripStatus / assignVehicleAndStart),
a per-city FleetEngineCfg (encrypted service-account JSON), and self-signed
scoped-JWT minting (consumer/driver/server) reusing Kernel.Utils.JWT.

createTrip is keyed on the BPP ride id so re-issues are idempotent
(ALREADY_EXISTS treated as success). Foundation for rider/driver journey
sharing; no call sites yet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R4VptpJt2kd4GQSDXcWL1c
Satisfy the pre-commit CI hook: sort the FleetEngine modules alphabetically in
the cabal, convert TokenScope trailing haddocks to leading, and fix the
createTrip guard-body indentation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R4VptpJt2kd4GQSDXcWL1c
@prakharritik prakharritik force-pushed the claude/fleet-engine-odrd-sdk branch from b15d415 to 800c68c Compare July 1, 2026 11:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants