Adds /api/sync/push/:provider/:routeId as the user-triggered entry
point for pushing a route. The route action delegates to a new
pushes.server.ts pipeline that:
- resolves the latest route_versions row and uses that GPX (not
routes.gpx) so the bytes Wahoo gets match the snapshot the user
sees
- short-circuits on (user, route, version, provider) idempotency:
a successful prior push returns the existing remote_id without
re-calling Wahoo
- detects scope_missing before hitting Wahoo and redirects through
getAuthUrl with a base64url-encoded state carrying pushAfter +
returnTo
- refreshes tokens once on PushError({ code: "token_expired" }) and
retries the push, then updates sync_connections in place
- records every outcome in sync_pushes (insert on first attempt,
update on retry) so the UI can show success/failure state
The OAuth callback handler now decodes the state, resumes a
pushAfter pipeline server-side after exchangeCode, and handles the
?error=access_denied path with a needs_permission notice.
Also flips packages/fit/src/fitsdk.d.ts to a regular .ts side-effect
shim so journal's tsc picks up the @garmin/fitsdk module declaration
when consuming the workspace package via source.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Extends the SyncProvider interface with an optional pushRoute method
plus PushRoutePayload, PushRouteResult, and a typed PushError so the
slice-4 action route can map error codes to user-facing copy without
parsing HTTP statuses itself.
Wahoo provider:
- adds routes_write to the requested OAuth scope set
- implements pushRoute: base64-encodes the FIT, builds the form body
Wahoo's POST /v1/routes expects, parses the remote id out of the
response, and throws typed PushError on 401/403/422/429/5xx
- saveConnection now persists the requested scopes as grantedScopes
on exchangeCode (Wahoo doesn't return a scope field, so the
requested set is the granted set)
Token refresh on 401 is deferred to the slice-4 action route — same
pattern as the existing webhook handler in this repo, which does
inline refresh rather than wrapping every call.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>