trails/openspec/changes/archive/2026-05-08-deepen-connected-services/specs/wahoo-import/spec.md
Ullrich Schäfer 7cd785e937
Archive deepen-connected-services + sync spec deltas
Task 7.3: applies the MODIFIED + ADDED requirements from
changes/deepen-connected-services/specs/ into openspec/specs/:

- connected-services/spec.md:
  - MODIFIED: OAuth token storage (renamed sync_connections → connected_services
    with credential_kind discriminator + JSONB credentials shape).
  - ADDED: Capability seams for providers (Importer / RoutePusher /
    WebhookReceiver per ADR-0002, no unified SyncProvider).
  - ADDED: Centralized credential lifecycle via ConnectedServiceManager.

- wahoo-import/spec.md: Provider-agnostic framework rewritten to reference
  capability seams + per-provider manifest. Token refresh now goes through
  withFreshCredentials. Renames sync_connections → connected_services
  throughout.

- wahoo-route-push/spec.md: Renames sync_connections → connected_services.
  ADDED: RoutePusher capability seam — shape (service, route) →
  {remoteId, version} per ADR-0003; Wahoo workarounds stay inside the
  adapter.

Change directory moved to openspec/changes/archive/2026-05-08-deepen-connected-services/.

29/29 tasks complete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 01:36:19 +02:00

3.7 KiB

MODIFIED Requirements

Requirement: Provider-agnostic sync framework

The system SHALL provide capability-shaped seams for external activity sync providers. Each provider declares a manifest at providers/<name>/manifest.ts listing its credential_kind (oauth | web-login | device) and the capability adapters it implements (Importer, RoutePusher, WebhookReceiver). There SHALL NOT be a unified SyncProvider interface containing every capability with optional methods.

Scenario: Add new provider

  • WHEN a developer wants to add a new sync provider (e.g., Garmin)
  • THEN they create providers/garmin/ containing a manifest.ts declaring credential_kind and the implemented capabilities
  • AND they implement only the capability interfaces that apply (e.g. Importer for an import-only provider)
  • AND they register the manifest in providers/registry.ts
  • AND OAuth flows, webhook routing, and settings UI work without further changes

Requirement: Connect Wahoo account

Users SHALL be able to connect their Wahoo account via OAuth2.

Scenario: Connect Wahoo

  • WHEN a user clicks "Connect Wahoo" in journal settings
  • THEN they are redirected to Wahoo's OAuth authorization page with scopes workouts_read, user_read, offline_data, routes_write
  • AND after granting permission, redirected back to the journal
  • AND access and refresh tokens are stored in connected_services (in the credentials JSONB blob, with credential_kind = 'oauth')
  • AND the granted scopes are recorded in connected_services.granted_scopes so feature gates can detect missing scopes without round-tripping to Wahoo

Scenario: Disconnect Wahoo

  • WHEN a user clicks "Disconnect" next to their Wahoo connection
  • THEN the stored credentials are deleted from connected_services

Scenario: Token refresh

  • WHEN a Wahoo API call fails with an expired token
  • THEN the OAuth CredentialAdapter is invoked via ConnectedServiceManager.withFreshCredentials to obtain a new access token automatically
  • AND the new tokens are written back to the credentials JSONB blob

Scenario: Existing connection without routes_write

  • WHEN a user connected before the routes_write scope was added attempts an action that requires it (such as pushing a route)
  • THEN the system detects the missing scope from connected_services.granted_scopes and routes the user through OAuth re-authorization to grant the new scope
  • AND the existing tokens remain valid until re-auth completes; ongoing read flows (workout import, webhook ingestion) continue to work

Requirement: Webhook-based automatic sync

New Wahoo workouts SHALL be automatically imported when they complete.

Scenario: Webhook receives new workout

  • WHEN Wahoo sends a workout_summary webhook to /api/sync/webhook/wahoo
  • THEN the system identifies the user via provider_user_id
  • AND downloads the FIT file from Wahoo's CDN (without auth headers, as CDN URLs are pre-signed)
  • AND converts it to GPX
  • AND creates a journal activity with the GPX, stats, and PostGIS geometry
  • AND records the import in sync_imports to prevent duplicates

Scenario: Webhook for workout without file

  • WHEN a webhook arrives for a workout with no FIT file URL
  • THEN the activity is created without GPX or geometry

Scenario: Duplicate webhook

  • WHEN a webhook arrives for a workout already imported
  • THEN the import is skipped silently (idempotent)

Scenario: Unknown user webhook

  • WHEN a webhook arrives with a provider_user_id not matching any connection
  • THEN the request is ignored with a 200 response (don't reveal user existence)