Skip to content

Add gett plugin (Gett taxi — Israel consumer account) - #43

Open
amitayks wants to merge 5 commits into
Michaelliv:mainfrom
amitayks:add-gett-plugin
Open

amitayks wants to merge 5 commits into
Michaelliv:mainfrom
amitayks:add-gett-plugin

Conversation

@amitayks

@amitayks amitayks commented Aug 8, 2026

Copy link
Copy Markdown

Adds a gett plugin: search places, price rides, and book/cancel real taxis on the owner's own Gett account.

Why the consumer surface (not the Business API)

Gett's official business-api.gett.com is gated on a per-company order entitlement that isn't self-serve (proven server-side: the portal's own business_api_clients/scopes returns only finance,employee). So this plugin rides the same JSON REST surface the phone app uses (b2cgateway.gett.com), reverse-engineered from an owner-driven capture.

Shape (follows the repo conventions)

  • Single self-contained packages/runline-plugins/gett/src/index.ts, direct fetch, no external deps.
  • Object-literal inputSchema/setConnectionSchema like the other plugins.
  • Refresh-token + saved-card rotation persisted via ctx.updateConnection (same pattern as gmail/googleCalendar).
  • access on every action + an ACCESS_CHECKLIST.md row + icon + README table row.

Actions (8)

Reads: whoami, find_place, price, nearby_drivers, ride_status
Writes: connect (owner login: phone → SMS OTP → card-digits MFA; a trusted device skips MFA), book_ride, cancel_ride

Safety

Purchase-eligible by design — booking a ride spends money and summons a real car. book_ride returns a priced preview + requiresConfirmation and only places a real order when the connection sets allowOrdering: true and the caller passes confirm: true. cancel_ride is likewise gated. Single-tenant: one account per connection.

Verification

  • bun --filter runline build passes; tsc --noEmit clean for the plugin.
  • Full cold cycle exercised live end-to-end against a real account (2026-08-11): fresh SMS onboarding → OTP verify → IL→GL token conversion → GL/GL tokens persisted → saved card auto-discovered → pricereal order booked (101764175, driver assigned) → tracked (Routing → Confirmed) → cancelled inside the free window (no charge)refresh rotated the token on demand. Every call returned 200, verified both through mitmproxy and direct.

Token refresh (resolved)

An earlier revision suspected a ~15-min access-token TTL forced periodic re-login. That was wrong. The real cause of the 400s was a missing ?lc=en query param on /auth/token — the app sends it on every call and the server returns a bare 400 (empty body) without it. With ?lc=en on both /auth/token calls (the refresh and the initial IL→GL conversion), refresh is fully headless and the session is long-lived and unattended: the GL refresh token lasts ~90 days and the grant does not need a live access token (a bearer expired by days still refreshes), so no keepalive schedule is required. Isolated via a controlled live probe — session token, x-user-location, and bearer casing were all irrelevant; ?lc=en alone flips 400 → 200.

Amitay Keisar and others added 5 commits August 8, 2026 21:04
Self-contained plugin for the owner's private Gett account via the mobile
app surface (b2cgateway.gett.com); the official Business API is gated on a
per-company 'order' entitlement.

- headless connect flow: phone + SMS OTP + card-digits MFA (trusted-device
  shortcut skips MFA); refresh token + saved card persisted via
  ctx.updateConnection; card auto-discovered on connect
- reads: whoami, find_place, price, nearby_drivers, ride_status
- gated writes: book_ride (priced preview -> confirm places a real order on
  the saved card), cancel_ride — behind allowOrdering + confirm:true
- access annotations on all 8 actions + ACCESS_CHECKLIST row + icon + README
- refresh() forces a token rotation
- connect status returns a keepalive hint { every: '10m', prompt: 'call gett.refresh()' }
- Gett's access token is ~15 min and refresh needs a live access token, so the
  cadence MUST be under 15 min; a missed window forces an SMS re-login. Documented
  the constraint (confirmed live: expires_in=900, all bearer variants 400).
connect() only stripped separators from the phone; a national number like
0526471797 (or 052-647-1797) stayed as-is while the challenge body hardcodes
country_phone_prefix:972. Gett then returns otp_sent but silently drops the SMS.
Add normPhone(): 00-exit/national-0/bare-local all map to 972XXXXXXXXX, idempotent
for already-international input. Schema descriptions note local digits are accepted.
The challenge endpoint returns HTTP 200 even when Gett refuses to send the SMS
(rc:3 status:"blocked" after too many attempts, with a blocked_until minute
count). connect() only checked the HTTP status, so it reported step:"otp_sent"
and the caller waited for a code that never came. Inspect the body verdict: on
rc!=0 / status!=success return step:"blocked"|"error" with retry_after_minutes
and the reason, so the agent tells the user to wait rather than ask for a code.
Both /auth/token calls (the accessToken refresh and the finishTokens IL→GL
conversion) now carry ?lc=en. Without the query param Gett returns a bare 400
(empty body) — this, not the ~15-min access-token TTL, was the real lockout.

Proven end-to-end 2026-08-11: routed the engine through mitmproxy, diffed our
bytes against the app's successful conversion, and a live probe isolated ?lc=en
as the sole cause (session token, x-user-location, bearer casing all irrelevant
— GL→GL refresh returns 200 with just ?lc=en and no session token). Verified a
full cold cycle: SMS onboarding → IL→GL → GL/GL tokens → refresh → real order
booked + cancelled, all 200, both via proxy and direct.

Consequently the session is long-lived/unattended: dropped the incorrect
'schedule refresh every <15 min or you're locked out' guidance from the refresh
action, connectStatus keepalive hint, and docblock — the refresh token lasts
~90d and a stored session survives with no keepalive schedule.
@amitayks

Copy link
Copy Markdown
Author

Update: the token-refresh limitation flagged above is resolved (591d527). Root cause was a missing ?lc=en query param on /auth/token — not the access-token TTL. Both /auth/token calls now carry it; refresh is fully headless and the session is long-lived/unattended (no keepalive schedule needed). Verified with a full cold cycle live: SMS onboarding → IL→GL conversion → real order booked + cancelled → on-demand refresh, all 200.

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.

1 participant