Project: Car Pulse Tracker (bnc-cpt) Date: 2026-02-16 Status: PROPOSAL Affects: bnc-cpt-api, bnc-cpt-utl, bnc-cpt-cnf
The Tesla Fleet API requires OAuth callback URLs registered in the Tesla Developer Portal.
Currently only https://dev.api.carpulsetracker.com/api/v1/tesla/oauth/callback is registered.
When a developer runs the API locally (on a dev box or in a GitHub Actions worker),
Tesla has no way to deliver the OAuth callback to localhost:8100 because:
dev.api.carpulsetracker.com)PRODUCTION/DEV DEPLOYED FLOW (works):
=============================================================================
Browser Tesla Auth Cloud Run (dev)
| | |
|--- POST /oauth/initiate ->| |
|<-- authUrl (tesla.com) ---| |
| | |
|--- GET auth.tesla.com --->| |
| (user logs in) | |
|<-- 302 redirect ---------| |
| | |
|--- GET /oauth/callback?code=xxx ----------------->| <-- THIS WORKS
|<-- 302 ?oauth=success ----------------------------|
| | |
LOCAL DEVELOPMENT FLOW (broken):
=============================================================================
Browser Tesla Auth localhost:8100
| | |
|--- POST /oauth/initiate ->| |
|<-- authUrl (tesla.com) ---| |
| | |
|--- GET auth.tesla.com --->| |
| (user logs in) | |
|<-- 302 redirect ---------| |
| | |
|--- GET dev.api.carpulsetracker.com/callback ------>| Cloud Run (dev)
| | ^^^^^^^^^^^^^^^^^^^^^^^^^
| | GOES TO CLOUD RUN,
| | NOT TO LOCAL INSTANCE!
| | |
| localhost:8100 NEVER receives the callback |
| Scenario | Tesla OAuth Testable? | Report Flow Testable? |
|---|---|---|
| Cloud Run (dev/tst/prd) | YES | YES |
| Local developer box | NO | NO (mock only) |
| GitHub Actions worker | NO | NO (mock only) |
The mock server (con-bnc-cpt-tesla-mock:9100) helps with unit tests but
cannot validate the real Tesla OAuth handshake, real token exchange,
or real vehicle data fetching.
Introduce a proxy relay mode in the deployed API (dev.api.carpulsetracker.com)
that acts as a transparent bridge between Tesla and the developer's local API instance.
The deployed API becomes a thin relay — it receives Tesla's OAuth callback, then forwards it to the developer's registered local tunnel endpoint. The local API processes the callback, fetches vehicle data, and returns the result through the relay back to the user's browser.
+--------------------------+
| Tesla Auth Server |
| auth.tesla.com |
+-----------+--------------+
|
(2) User authenticates
(3) Tesla redirects to
registered callback URL
|
v
+----------+ +---------------------------+ +-----------------+
| | (1) initiate| Cloud Run (dev) | (4) relay | Local API |
| Browser | ------------>| dev.api.carpulsetracker | ----------->| localhost:8100 |
| | | .com | | (or GH worker) |
| | | | | |
| | | /api/v1/tesla/oauth/ | (5) local | Processes |
| | | callback?code=xxx | processes | OAuth callback |
| | | |<------------| Returns result |
| |<-------------| (6) Relay response back | | |
| | redirect | to browser | | |
+----------+ +---------------------------+ +-----------------+
|
| (4) Also talks to Tesla
| Fleet API for token
| exchange & data fetch
v
+--------------------------+
| Tesla Fleet API |
| fleet-api.prd.eu.vn. |
| cloud.tesla.com |
+--------------------------+
| Decision | Choice | Rationale |
|---|---|---|
| Tunnel technology | Cloudflare Tunnel (cloudflared) | Free, stable, no random URLs — provides deterministic subdomain |
| Relay trigger | X-Dev-Proxy-Target header or /proxy/register endpoint |
Developer registers their tunnel URL with the deployed API |
| Session isolation | Per-state proxy routing | Each OAuth state token maps to a specific developer's tunnel |
| Security | Shared secret + HMAC | Only authorized developers can register proxy targets |
| Scope | dev environment only | Proxy relay is disabled in tst/prd via env var |
A new FastAPI router /api/v1/dev-proxy/ provides:
POST /api/v1/dev-proxy/register — Register a local tunnel endpoint
DELETE /api/v1/dev-proxy/unregister — Remove registration
GET /api/v1/dev-proxy/status — Check active registrations
Registration payload:
{
"tunnel_url": "https://my-dev-abc123.trycloudflare.com",
"developer_id": "ysg",
"hmac_signature": "<HMAC-SHA256 of tunnel_url + timestamp using shared secret>"
}
Storage: In-memory dict with TTL (same pattern as TeslaFleetService._states).
No database needed — registrations are ephemeral (TTL: 4 hours).
When the /api/v1/tesla/oauth/callback endpoint receives a request:
state parameter maps to a registered proxy target# Pseudocode for relay logic in tesla.py callback handler
async def handle_tesla_oauth_callback(request, code, state, ...):
# Check if this state was initiated by a proxied developer
proxy_target = DevProxyRegistry.get_target_for_state(state)
if proxy_target:
# RELAY MODE: Forward to developer's local API via tunnel
return await relay_callback_to_target(proxy_target, request)
else:
# NORMAL MODE: Process locally on Cloud Run
return await process_callback_locally(code, state, ...)
A new shell action do_start_dev_proxy_tunnel that:
cloudflared tunnel exposing localhost:8100dev.api.carpulsetracker.com/api/v1/dev-proxy/registerTESLA_REDIRECT_URI to use the production callback URL
(since callbacks will be relayed through Cloud Run)Developer's Machine Cloud Run (dev)
===================== =====================
$ ./run -a do_start_dev_proxy_tunnel
|
v
[1] Start cloudflared tunnel
cloudflared tunnel --url localhost:8100
|
v
[2] Tunnel assigns URL
https://dev-ysg-abc123.trycloudflare.com
|
v
[3] Register with Cloud Run
POST https://dev.api.carpulsetracker.com
/api/v1/dev-proxy/register
Body: {
tunnel_url: "https://dev-ysg-abc123...",
developer_id: "ysg",
hmac_signature: "..."
}
| |
+----------------------------------------->|
|
[4] Validate HMAC
[5] Store registration:
dev_proxies["ysg"] = {
tunnel_url: "https://...",
registered_at: now(),
ttl: 4h
}
|
|<-----------------------------------------+
v 200 OK
[6] Registration confirmed
"Dev proxy active for ysg"
|
v
[7] Start local API (uvicorn)
TESLA_REDIRECT_URI=https://dev.api.carpulsetracker.com/...
python run.py
|
v
[8] Ready: local API serves on :8100
Tunnel exposes it publicly
Tesla callbacks will be relayed
Browser Cloud Run (dev) Tesla Auth Local API (:8100) Tesla Fleet API
| | | | |
| [1] POST /oauth/initiate (via tunnel) | |
|------------------>| - - relay - - - ->| | |
| | | | |
| | [2] Forward to local API | |
| |-------------------------------------->| |
| | | | |
| | | [3] initiate_oauth() |
| | | state="abc123" | |
| | | Register state -> proxy_target |
| | | | |
| | [4] Return authUrl | |
| |<--------------------------------------| |
|<------------------| | | |
| | | | |
| [5] Redirect to auth.tesla.com | | |
|-------------------------------------->| | |
| | | | |
| [6] User logs in, grants consent | | |
|<--------------------------------------| | |
| | | | |
| [7] Tesla redirects to registered callback URL | |
| GET dev.api.carpulsetracker.com/api/v1/tesla/oauth/callback?code=XYZ&state=abc123
|------------------>| | | |
| | | | |
| | [8] Lookup: state "abc123" has proxy target |
| | | | |
| | [9] RELAY: Forward callback to tunnel |
| |-------------------------------------->| |
| | | | |
| | | [10] validate_state("abc123") |
| | | [11] Exchange code for token |
| | | POST tesla token endpoint |
| | | |--------------------->|
| | | |<---------------------|
| | | | token_data |
| | | | |
| | | [12] Fetch vehicle data |
| | | GET /api/1/vehicles |
| | | GET /api/1/vehicles/{id}/data |
| | | |--------------------->|
| | | |<---------------------|
| | | | vehicle data |
| | | | |
| | | [13] Store report, return session_id |
| | | | |
| | [14] Response: redirect to WUI | |
| |<--------------------------------------| |
| | | | |
| [15] Relay redirect back to browser | | |
|<------------------| | | |
| | | | |
| [16] Browser loads WUI ?oauth=success&session_id=xyz | |
| | | | |
| [17] GET /tesla/report/{session_id} | | |
|------------------>| - - relay - - - ->| | |
| |-------------------------------------->| |
| |<--------------------------------------| |
|<------------------| [18] Full vehicle report returned | |
| | | | |
GitHub Actions Runner Cloud Run (dev) Tesla Auth
======================== ===================== =============
[1] CI workflow starts
.github/workflows/ci-tesla-e2e.yaml
|
v
[2] Clone repos, build containers
make do-setup-api-no-cache
|
v
[3] Start cloudflared tunnel in CI
cloudflared tunnel --url http://con-bnc-cpt-api:8100
|
v
[4] Register tunnel with Cloud Run
POST dev.api.carpulsetracker.com/api/v1/dev-proxy/register
Body: {
tunnel_url: "https://ci-run-12345.trycloudflare.com",
developer_id: "github-ci-${{ github.run_id }}",
hmac_signature: "..."
}
| |
+-------------------------------------->|
| [5] Store CI registration
|<--------------------------------------+
v
[6] Run e2e Tesla OAuth test
- Puppeteer opens WUI at localhost:3333
- Clicks "Connect Tesla"
- OAuth flow executes:
* Tesla callback hits Cloud Run
* Cloud Run relays to CI tunnel
* CI API processes callback
* Fetches real Tesla data
- Test verifies report generated
|
v
[7] Deregister tunnel
DELETE dev.api.carpulsetracker.com/api/v1/dev-proxy/unregister
|
v
[8] Cleanup, report results
+-------------------+ +-------------------+ +---------------------+
| Developer Box | | Cloud Run (dev) | | In-Memory Store |
| or CI Runner | | FastAPI | | (DevProxyRegistry) |
+-------------------+ +-------------------+ +---------------------+
| | |
| POST /dev-proxy/register | |
| { | |
| tunnel_url, | |
| developer_id, | |
| hmac_signature, | |
| timestamp | |
| } | |
|--------------------------->| |
| | |
| [Validate HMAC] |
| [Check timestamp |
| freshness < 60s] |
| | |
| | store(developer_id, { |
| | tunnel_url, |
| | registered_at, |
| | ttl: 4h |
| | }) |
| |--------------------------->|
| | |
| | 201 Created |
|<---------------------------| |
| | |
When a proxied developer calls POST /oauth/initiate:
+-------------------+ +---------------------+ +---------------------+
| Local API | | DevProxyRegistry | | TeslaFleetService |
| (via tunnel) | | (state → target) | | (_states dict) |
+-------------------+ +---------------------+ +---------------------+
| | |
| initiate_oauth( | |
| paymentId, plan) | |
| | |
| [Generate state token] |
| state = "abc123..." |
| | |
| [Map state to proxy target] |
| state_proxy_map["abc123"] = { |
| tunnel_url: "https://...", |
| developer_id: "ysg" |
| } |
| |--------------------------->|
| | Also store in _states: |
| | _states["abc123"] = { |
| | paymentId, plan, |
| | created_at, proxy_target |
| | } |
| | |
|<---------------------------| |
| authUrl with state=abc123 | |
| | |
+----------+ +-----------------------+ +------------------+ +--------------+
| Browser | | Cloud Run (dev) | | Tunnel (cfd) | | Local API |
| | | Relay Middleware | | *.trycloudflare | | :8100 |
+----------+ +-----------------------+ +------------------+ +--------------+
| | | |
| GET /callback | | |
| ?code=XYZ | | |
| &state=abc123 | | |
|------------------->| | |
| | | |
| [Lookup state "abc123" | |
| in state_proxy_map] | |
| | | |
| [Found! proxy_target = | |
| "https://dev-ysg-..."] | |
| | | |
| [Build relay request: | |
| GET {tunnel}/api/v1/tesla/ | |
| oauth/callback?code=XYZ | |
| &state=abc123 | |
| Headers: X-Forwarded-For, | |
| X-Relay-From, X-Relay-Hmac] | |
| | | |
| |--- HTTPS GET ------------>| |
| | |--- HTTP GET --------->|
| | | |
| | | [Process callback: |
| | | validate_state() |
| | | exchange code |
| | | fetch vehicle data |
| | | store report] |
| | | |
| | |<-- 302 Redirect ------|
| | | Location: frontend |
| | | ?oauth=success |
| | | &session_id=xyz |
| |<--- HTTPS 302 ------------| |
| | | |
| [Relay: pass through | |
| the 302 redirect to browser] | |
| | | |
|<--- 302 Redirect --| | |
| Location: | | |
| localhost:3333 | | |
| ?oauth=success | | |
| &session_id=xyz | | |
| | | |
+========================================================================================+
| DATA ENTITIES & THEIR LOCATIONS |
+========================================================================================+
| |
| CLOUD RUN (dev.api.carpulsetracker.com) |
| +------------------------------------------------------------------+ |
| | DevProxyRegistry (in-memory, TTL: 4h) | |
| | +------------------------------------------------------------+ | |
| | | dev_proxies = { | | |
| | | "ysg": { | | |
| | | tunnel_url: "https://dev-ysg-abc.trycloudflare.com", | | |
| | | registered_at: "2026-02-16T10:00:00Z", | | |
| | | ttl: 14400 (4 hours) | | |
| | | }, | | |
| | | "github-ci-12345": { | | |
| | | tunnel_url: "https://ci-12345.trycloudflare.com", | | |
| | | registered_at: "2026-02-16T11:30:00Z", | | |
| | | ttl: 14400 | | |
| | | } | | |
| | | } | | |
| | +------------------------------------------------------------+ | |
| | | |
| | state_proxy_map = { | |
| | "state_token_abc123": { | |
| | developer_id: "ysg", | |
| | tunnel_url: "https://dev-ysg-abc.trycloudflare.com" | |
| | } | |
| | } | |
| +------------------------------------------------------------------+ |
| |
| LOCAL API (localhost:8100) |
| +------------------------------------------------------------------+ |
| | TeslaFleetService._states = { | |
| | "state_token_abc123": { | |
| | paymentId: "pi_test_123", | |
| | plan: "basic", | |
| | created_at: "2026-02-16T10:05:00Z" | |
| | } | |
| | } | |
| | | |
| | TeslaFleetService._reports = { | |
| | "session_xyz789": { | |
| | report: { vehicle: {...}, vehicleData: {...}, ... }, | |
| | state_data: { paymentId: "pi_test_123", plan: "basic" }, | |
| | created_at: "2026-02-16T10:05:30Z" | |
| | } | |
| | } | |
| +------------------------------------------------------------------+ |
| |
+========================================================================================+
app/services/dev_proxy.pyPurpose: DevProxyRegistry class — manages tunnel registrations and state-to-proxy mappings
Pattern: Follows TeslaFleetService pattern (class-level dicts, TTL cleanup)
Key methods:
register(tunnel_url, developer_id, hmac_sig) -> bool
unregister(developer_id) -> bool
get_target_for_state(state) -> Optional[str]
map_state_to_proxy(state, developer_id) -> None
cleanup_expired() -> None
is_enabled() -> bool (checks DEV_PROXY_ENABLED env var)
app/api/v1/routers/dev_proxy.pyPurpose: REST endpoints for proxy registration
Endpoints:
POST /api/v1/dev-proxy/register — Register tunnel URL
DELETE /api/v1/dev-proxy/unregister — Remove registration
GET /api/v1/dev-proxy/status — List active registrations
app/api/v1/routers/tesla.pyChanges to handle_tesla_oauth_callback():
- Before processing: check DevProxyRegistry.get_target_for_state(state)
- If proxy target found: relay request via httpx to tunnel URL
- If no proxy target: process normally (existing code, unchanged)
Changes to initiate_tesla_oauth():
- If request came through proxy (X-Relay-From header): register state mapping
app/config.pyNew settings:
DEV_PROXY_ENABLED: bool = False # Must be True to activate
DEV_PROXY_SHARED_SECRET: Optional[str] # HMAC shared secret
DEV_PROXY_MAX_TTL: int = 14400 # 4 hours in seconds
dev.env.yaml# Add to step 029-create-gcp-secrets:
DEV_PROXY_SHARED_SECRET:
secret_name: "bnc-cpt-dev-proxy-shared-secret"
version: "latest"
# Add to step 030-gcp-cloud-run environment_variables:
DEV_PROXY_ENABLED: "True" # Only in dev!
tst.env.yaml, prd.env.yaml# DEV_PROXY_ENABLED defaults to False — proxy is disabled in tst/prd
src/bash/run/start-dev-proxy-tunnel.func.shdo_start_dev_proxy_tunnel()
# 1. Check cloudflared is installed
# 2. Start tunnel: cloudflared tunnel --url localhost:${API_PORT_HOST:-8100}
# 3. Capture tunnel URL from cloudflared output
# 4. Register with Cloud Run: POST /api/v1/dev-proxy/register
# 5. Export TESLA_REDIRECT_URI=https://dev.api.carpulsetracker.com/...
# 6. Print status and instructions
src/bash/run/stop-dev-proxy-tunnel.func.shdo_stop_dev_proxy_tunnel()
# 1. Deregister from Cloud Run: DELETE /api/v1/dev-proxy/unregister
# 2. Kill cloudflared process
# 3. Print confirmation
src/bash/run/ci-start-dev-proxy.func.shdo_ci_start_dev_proxy()
# 1. Install cloudflared (apt-get or binary download)
# 2. Start tunnel to container API (http://con-bnc-cpt-api:8100)
# 3. Register with Cloud Run
# 4. Used by GitHub Actions workflow for e2e Tesla tests
src/docker/.env# Add:
DEV_PROXY_ENABLED=True
DEV_PROXY_SHARED_SECRET=local-dev-shared-secret-change-in-prod
src/docker/docker-compose-app.yaml# Add to cpt-api service environment:
DEV_PROXY_ENABLED: "${DEV_PROXY_ENABLED:-False}"
DEV_PROXY_SHARED_SECRET: "${DEV_PROXY_SHARED_SECRET:-}"
| Threat | Mitigation |
|---|---|
| Unauthorized proxy registration | HMAC-SHA256 signature with shared secret |
| Replay attacks | Timestamp in HMAC payload, 60s freshness window |
| Proxy target hijacking | Registration overwrites by developer_id (latest wins) |
| Data exfiltration via rogue tunnel | Only dev environment; tst/prd have proxy DISABLED |
| Stale registrations | Auto-expire after TTL (4 hours) |
| SSRF via tunnel_url | Validate URL format; only HTTPS allowed; only *.trycloudflare.com |
message = f"{tunnel_url}:{developer_id}:{timestamp_unix}"
signature = HMAC-SHA256(shared_secret, message)
Validation:
1. Recompute HMAC with stored shared secret
2. Compare signatures (constant-time comparison)
3. Check timestamp is within 60 seconds of server time
# In dev_proxy.py
class DevProxyRegistry:
@classmethod
def is_enabled(cls) -> bool:
return settings.DEV_PROXY_ENABLED and settings.ENV in ("dev", "lcl", "inf")
The proxy relay is impossible to activate in tst or prd environments,
even if someone sets DEV_PROXY_ENABLED=True, because the environment guard
also checks the ENV variable.
Rejected. Tesla requires pre-registered callback URLs. Random tunnel URLs cannot be registered with Tesla's developer portal.
Cloud Run receives callback → stores code+state in GCS bucket
Local API polls GCS bucket → picks up callback → processes it
Rejected. Adds latency (polling interval), requires GCS access from local, and complicates the browser redirect flow (browser waits on Cloud Run, which waits for local to process and signal completion).
Local API maintains WebSocket to Cloud Run
Cloud Run forwards callbacks over WebSocket in real-time
Rejected. Cloud Run has a 60-minute WebSocket timeout. Developers would need to reconnect periodically. Adds complexity for marginal benefit over HTTP relay.
/etc/hosts: 127.0.0.1 dev.api.carpulsetracker.com
SSH tunnel to forward port 443 to localhost:8100
Rejected. Requires root/sudo for hosts file and port 443. Breaks other team members' ability to reach the real dev API. Does not work in CI.
Rejected. Tesla developer portal limits the number of apps. Each app requires separate review. Not scalable for team development.
tests/test_dev_proxy.py
- test_register_valid_hmac → 201
- test_register_invalid_hmac → 403
- test_register_expired_timestamp → 403
- test_unregister → 200
- test_proxy_disabled_returns_404
- test_callback_with_proxy_state_relays
- test_callback_without_proxy_state_processes_locally
- test_registration_ttl_expiry
- test_only_https_tunnel_urls_accepted
- test_ssrf_protection_rejects_non_cloudflare_urls
test/specs/test-tesla-oauth-e2e.spec.js
- Start tunnel
- Register proxy
- Navigate WUI → Payment → OAuth
- Complete Tesla login (test account)
- Verify callback relayed to local API
- Verify report displayed in WUI
# .github/workflows/ci-tesla-e2e.yaml
# Triggered: manual (workflow_dispatch) — not on every push
# Reason: requires real Tesla test credentials
steps:
- name: Start tunnel and proxy
run: ./run -a do_ci_start_dev_proxy
- name: Run Tesla e2e test
run: make test-api-tesla-e2e
- name: Cleanup
run: ./run -a do_ci_stop_dev_proxy
| Project | Path | Purpose |
|---|---|---|
| bnc-cpt-api | app/services/dev_proxy.py |
Proxy registry service |
| bnc-cpt-api | app/api/v1/routers/dev_proxy.py |
REST endpoints for proxy mgmt |
| bnc-cpt-api | tests/test_dev_proxy.py |
Unit tests for proxy |
| bnc-cpt-utl | src/bash/run/start-dev-proxy-tunnel.func.sh |
Shell action: start tunnel |
| bnc-cpt-utl | src/bash/run/stop-dev-proxy-tunnel.func.sh |
Shell action: stop tunnel |
| bnc-cpt-utl | src/bash/run/ci-start-dev-proxy.func.sh |
Shell action: CI tunnel setup |
| bnc-cpt-utl | src/bash/run/ci-stop-dev-proxy.func.sh |
Shell action: CI tunnel teardown |
| bnc-cpt-utl | doc/md/proxy-proposal.md |
This document |
| Project | Path | Change |
|---|---|---|
| bnc-cpt-api | app/config.py |
Add DEV_PROXY_* settings |
| bnc-cpt-api | app/api/v1/routers/tesla.py |
Add relay check in callback |
| bnc-cpt-api | app/main.py |
Include dev_proxy router |
| bnc-cpt-cnf | bnc-cpt/dev.env.yaml |
Add DEV_PROXY_ENABLED, secret |
| bnc-cpt-utl | src/docker/.env |
Add DEV_PROXY_* env vars |
| bnc-cpt-utl | src/docker/docker-compose-app.yaml |
Pass DEV_PROXY_* to container |
dev_proxy.py service in bnc-cpt-api/dev-proxy/ router endpointsconfig.pytesla.py callback handler with relay logicdo_start_dev_proxy_tunnel shell actiondo_stop_dev_proxy_tunnel shell action.env and docker-compose-app.yamldo_ci_start_dev_proxy shell actiondo_ci_stop_dev_proxy shell actionDEV_PROXY_SHARED_SECRET to GCP Secret Manager (dev only)dev.env.yaml with new config| Dependency | Status | Action |
|---|---|---|
cloudflared binary |
Not installed | Add to do_check_install_cloudflared shell action |
| Tesla test account credentials | Available in GCP Secrets | Already provisioned |
DEV_PROXY_SHARED_SECRET |
Not yet created | Create in GCP Secret Manager (dev env) |
httpx (async HTTP client) |
Already in bnc-cpt-api | Used by TeslaFleetService |
| Cloud Run (dev) redeployment | Required | After API changes, redeploy via CI |
./run -a do_start_dev_proxy_tunnel on their local boxlocalhost:3333 in browsertst or prd environments