@crvouga/mockingbird-service-fitbit 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog — @crvouga/mockingbird-service-fitbit
2
+
3
+ ## 0.2.0 (2026-10-21)
4
+
5
+ ### Features
6
+
7
+ - add manual response controls ([4599960](https://github.com/crvouga/mockingbird/commit/45999603e8b860c633181367afc7baa906ba486b))
8
+
9
+ ## 0.1.0 (2026-10-21)
10
+
11
+ Initial release.
package/DISCOVERY.md ADDED
@@ -0,0 +1,78 @@
1
+ # @crvouga/mockingbird-service-fitbit discovery
2
+
3
+ This is the installed-package index for coding agents and tooling. All relative links resolve
4
+ inside `node_modules/@crvouga/mockingbird-service-fitbit/`; no repository checkout is needed to discover the emulator's
5
+ supported surface or documented behavior.
6
+
7
+ ## Capability and behavior sources
8
+
9
+ | Question | Authoritative file | What it contains |
10
+ | --- | --- | --- |
11
+ | Behaviour and integration | [`README.md`](README.md) | Routes, state transitions, auth, webhooks, controls, presets and deliberate omissions. |
12
+ | Exact capabilities | [`SUPPORT.md`](SUPPORT.md) | Supported, unsupported and parity-covered operations or commands, including reasons for gaps. |
13
+ | Wire contract | [`openapi.yaml`](openapi.yaml) | Machine-readable paths, methods, schemas, responses and parity annotations. |
14
+ | Public API | [`dist/index.d.ts`](dist/index.d.ts) | The installed package's exact TypeScript exports and signatures. |
15
+ | Package metadata | [`package.json`](package.json) | Runtime/entry-point claims, vendor links, parity scope/tier and `mockingbird.discovery`. |
16
+
17
+ Read these together: the contract/capability matrix says *what* is available, while the README
18
+ defines stateful behavior, lifecycle rules, test controls, and intentional oracle differences.
19
+ If prose and an executable surface disagree, report a parity mismatch instead of adding a
20
+ consumer-side workaround.
21
+
22
+ ## Parity and oracle
23
+
24
+ - Declared parity surface: **OAuth PKCE, activity log, sleep and daily summary**.
25
+ - Parity tier: **cold** (the repository controls when live checks run).
26
+ - Oracle: **Live vendor API or sandbox**.
27
+ - Repository command: `bun run parity:service -- fitbit`.
28
+ - Evidence model: Run from a Mockingbird checkout; credentials come only from .env.local or GitHub Actions secrets. Missing credentials exit 2.
29
+
30
+ The npm package contains evidence summaries and the exact contract, not credentials or the
31
+ repository-only parity harness. Self-parity/property and acceptance tests run in the Mockingbird
32
+ repository; live parity is an additional oracle check, not a substitute for the packaged matrix.
33
+
34
+ ## Runtime introspection
35
+
36
+ - `GET /__admin/health`
37
+ - `GET /__admin`
38
+ - `GET /__admin/controls`
39
+ - `GET /__admin/state`
40
+ - `GET /__admin/requests`
41
+ - `GET /__admin/metrics`
42
+ - `GET /__admin/faults/presets`
43
+ - `GET /__admin/ui`
44
+
45
+ For HTTP services, use `x-mockingbird-namespace` (or the documented credential/path carrier) so
46
+ parallel tests do not share state. Admin state, journal, metrics and fault-preset endpoints are
47
+ designed for assertions and diagnosis by consuming test suites.
48
+
49
+ ## Admin controls
50
+
51
+ Every HTTP emulator takes the same controls, and each means the same thing at boot
52
+ (`createRuntime({ adminControls })`, `serve --admin-controls`), over `/__admin`, and in a request header:
53
+
54
+ | Control | Admin API | Request header | Interface |
55
+ | --- | --- | --- | --- |
56
+ | Isolate state by key | `GET /__admin/namespaces` | `x-mockingbird-namespace: <key>` | [Namespaces](https://mockingbird.chrisvouga.dev/docs/admin-controls/namespaces) |
57
+ | Reset to boot | `POST /__admin/reset` | `x-mockingbird-reset: true` | [Reset](https://mockingbird.chrisvouga.dev/docs/admin-controls/reset) |
58
+ | Snapshot and restore | `POST /__admin/snapshots`, `POST /__admin/snapshots/<id>/restore` | `x-mockingbird-snapshot: <name>`, `x-mockingbird-restore: <name>` | [Snapshot and restore](https://mockingbird.chrisvouga.dev/docs/admin-controls/snapshot-and-restore) |
59
+ | Move the clock | `POST /__admin/clock` | `x-mockingbird-time: <instant>`, `x-mockingbird-advance: <duration>` | [Clock](https://mockingbird.chrisvouga.dev/docs/admin-controls/clock) |
60
+ | Read or branch from old state | `GET /__admin/timeline` | `x-mockingbird-at: <checkpoint>`, `x-mockingbird-branch: <name>` | [Checkpoints and branches](https://mockingbird.chrisvouga.dev/docs/admin-controls/checkpoints-and-branches) |
61
+ | Inject a fault | `POST /__admin/faults` | `x-mockingbird-fault: <fault>` | [Faults](https://mockingbird.chrisvouga.dev/docs/admin-controls/faults) |
62
+ | Latency and timeouts | `PUT /__admin/latency` | `x-mockingbird-delay: <duration>`, `x-mockingbird-timeout: <duration>` | [Latency and timeouts](https://mockingbird.chrisvouga.dev/docs/admin-controls/latency-and-timeouts) |
63
+ | Replay from a seed | `PUT /__admin/controls` | `x-mockingbird-seed: <seed>` | [Seed and determinism](https://mockingbird.chrisvouga.dev/docs/admin-controls/seed-and-determinism) |
64
+ | See what happened | `GET /__admin/requests`, `GET /__admin/state` | | [Journal, metrics and state](https://mockingbird.chrisvouga.dev/docs/admin-controls/journal-and-state) |
65
+
66
+ `GET /__admin` lists every route and header with a one-line summary, and `GET /__admin/controls`
67
+ is the namespace's current control document. The same seed and the same requests give the same
68
+ bytes. Setting controls as an emulator starts is [Boot](https://mockingbird.chrisvouga.dev/docs/admin-controls/boot); every route and header is in
69
+ the [Reference](https://mockingbird.chrisvouga.dev/docs/admin-controls/reference). The full guide: <https://mockingbird.chrisvouga.dev/docs/admin-controls>.
70
+
71
+ ## Report a mismatch or missing capability
72
+
73
+ Follow the [agent reporting contract](https://github.com/crvouga/mockingbird/blob/main/docs/REPORTING_ISSUES.md). Include package version,
74
+ operation/command, a minimal redacted request, actual emulator result, expected oracle result or vendor
75
+ documentation, and whether the mismatch appears in the matrix. Never include keys, tokens,
76
+ customer data, prompts, PHI, card data, or unredacted recordings.
77
+
78
+ Service key: `fitbit`.
package/README.md CHANGED
@@ -1,3 +1,97 @@
1
- # Temporary Holding Version
1
+ # @crvouga/mockingbird-service-fitbit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > Local emulators. Real API contracts. Part of [Mockingbird](https://github.com/crvouga/mockingbird).
4
+
5
+ WIP Fitbit legacy Web API emulator: OAuth 2.0 authorization code with PKCE, refresh rotation and revoke, the profile, the activity log list, the sleep log by date and the daily activity summary, over synthetic data. Fitbit announced the end of support for the legacy Web API on 2026-09-30 and its shutdown on 2026-10-30 (migration target: the Google Health API); this emulator keeps the documented legacy contract testable after that.
6
+
7
+ ## Install
8
+
9
+ `bun add @crvouga/mockingbird-service-fitbit`
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { createServer } from "@crvouga/mockingbird-service-fitbit/server"
15
+
16
+ // Test setup: a local Fitbit. One origin serves the API, token, revoke and authorize routes, so it
17
+ // replaces https://api.fitbit.com and https://www.fitbit.com in the application.
18
+ const server = await createServer({
19
+ activityLogs: [
20
+ {
21
+ userId: "ABC123",
22
+ log: { logId: 1, activityName: "Walk", startTime: "2026-09-02T06:00:00.000-07:00", steps: 3000 },
23
+ },
24
+ ],
25
+ })
26
+ const baseUrl = server.url
27
+
28
+ // Application code: the requests it sends to Fitbit (default synthetic user ABC123 and its token).
29
+ const response = await fetch(
30
+ `${baseUrl}/1/user/-/activities/list.json?afterDate=2026-09-01&sort=asc&limit=20&offset=0`,
31
+ { headers: { authorization: "Bearer mock_fitbit_token", accept: "application/json" } },
32
+ )
33
+ const { activities, pagination } = await response.json()
34
+
35
+ await server.close()
36
+ ```
37
+
38
+ Run `mockingbird-fitbit serve --port 12141`. Fitbit publishes no JS SDK and the consumer uses raw HTTP, so inject the emulator origin as the API origin, the token origin, the revoke origin and the authorize page origin (one local origin serves all four). A browser or simulator reaches the authorize page over HTTP, so use the served emulator rather than in-process `fetch` for the consent step.
39
+
40
+ ## Routes
41
+
42
+ | Route | What it does |
43
+ | --- | --- |
44
+ | `GET /oauth2/authorize` | The consent page. Validates `client_id`, `redirect_uri`, `response_type=code`, `scope`, and the S256 `code_challenge`, then redirects (302) to the redirect URI with `code` and `state`, or with `error`, `error_description` and `state`. An unregistered client or redirect URI cannot be redirected and answers 400 HTML. |
45
+ | `POST /oauth2/token` | Basic client auth, form body. `authorization_code` (with `redirect_uri` and `code_verifier`) and `refresh_token` grants. Answers `access_token`, `refresh_token`, `expires_in: 28800`, `token_type: "Bearer"`, `scope`, `user_id`. |
46
+ | `POST /oauth2/revoke` | Basic client auth, form `token=<access or refresh token>`. 200; every token of that user for the client is gone. |
47
+ | `GET /1/user/{-\|encodedId}/profile.json` | `{ user: { encodedId, displayName, ... } }`. Scope `profile`. |
48
+ | `GET /1/user/-/activities/list.json` | `afterDate` (with `sort=asc`) or `beforeDate` (with `sort=desc`), `limit` (1 to 100), `offset`. Scope `activity`. |
49
+ | `GET /1/user/-/activities/date/{date}.json` | `{ activities, goals, summary }`. Scope `activity`. |
50
+ | `GET /1/user/-/sleep/date/{date}.json` and `/1.2/...` | `{ sleep, summary }`. Scope `sleep`. Both path versions serve the same data. |
51
+
52
+ Authorization codes are single use and last 10 minutes; access tokens last 28800 seconds on the emulator clock. A refresh issues a new access and refresh token for the same user and scopes and removes the previous pair (single-use rotation, so the old access token answers 401 and the old refresh token answers `invalid_grant`). A failed code exchange (wrong verifier, wrong redirect URI) does not consume the code. The default client is `mock_client` / `mock_client_secret` (any redirect URI unless `redirectUris` is seeded); the default user is `ABC123` with bearer `mock_fitbit_token` and refresh token `mock_fitbit_refresh`, all scopes, never expiring until the clock passes year 2100.
53
+
54
+ Data semantics: activity logs are filtered and ordered by the local wall-clock part of `startTime` (`afterDate` includes, `beforeDate` excludes); `pagination.next` is a full URL on the emulator origin with the next `offset` and is an empty string on the last page; `previous` likewise. A sleep log belongs to the day in `dateOfSleep` (else the date of `endTime`), so a log that began the previous evening is returned on the day it ended; `summary` totals `minutesAsleep` and `timeInBed` and sums `levels.summary.{deep,light,rem,wake}.minutes` into `summary.stages` when any log carries them. A day with no sleep logs is 200 with `{ sleep: [], summary: { totalMinutesAsleep: 0, totalSleepRecords: 0, totalTimeInBed: 0 } }`; a day with no activity data is 200 with zeroed `summary`, default `goals` and `activities: []`. Dates are `YYYY-MM-DD` (real calendar dates) or `today`. Logs are pass-through: every field you seed comes back.
55
+
56
+ ## Test controls
57
+
58
+ Seed with `createRuntime`/`createServer` options (`users`, `clients`, `grants`, `codes`, `activityLogs`, `sleepLogs`, `dailySummaries`, `consent`) or the admin routes below (all relative to `/__admin`, relocatable via `adminPrefix`):
59
+
60
+ | Admin route | Does |
61
+ | --- | --- |
62
+ | `PUT /consent` `{ mode: "approve" \| "deny" \| "error", userId? }` / `GET /consent` | What the authorize page does next, and which seeded user the code belongs to. |
63
+ | `POST /users` `{ encodedId, profile? }` | Seed a user; `profile` fields come back on the profile route. |
64
+ | `POST /activity-logs` / `POST /sleep-logs` `{ userId?, logs: [...] }` | Seed logs (`logId` and `startTime` are required; sleep also `endTime`). A repeated `logId` replaces the log. |
65
+ | `POST /daily-summaries` `{ userId?, date, activities?, goals?, summary? }` | Seed one day; fields merge over the defaults. |
66
+ | `POST /tokens/expire` `{ userId }` | Expire the user's access tokens now, without moving the clock; refresh tokens still work. |
67
+ | `POST /tokens/revoke` `{ userId }` | Revoke every token of the user, so the next refresh is `invalid_grant`. |
68
+
69
+ `POST /clock {"advance": ms}` moves the namespace clock: an access token older than 28800 seconds then answers 401 `expired_token`, and a code older than 10 minutes is refused. Standard `/__admin/health`, `/state` (collections `users`, `clients`, `grants`, `codes`, `activity_logs`, `sleep_logs`, `daily_summaries`, `settings`), `/reset`, `/requests`, Timeline checkpoints and faults are available. The header `x-mockingbird-namespace`, `/__admin/ns/<name>` paths and credential mappings (bearer token or Basic client id) isolate data. Journals contain metadata, never token or health payloads.
70
+
71
+ Presets (`POST /__admin/faults {"preset": "<name>", "count": n}`): `unauthorized` (401 `expired_token`), `forbidden` (403 `insufficient_scope`), `rate_limited` (429 with `Retry-After: 2` and `fitbit-rate-limit-*` headers), `rate_limited_no_retry_after`, `server_error` (500), `connection_drop`. No webhooks: the Subscription API is not modelled.
72
+
73
+ ## Evidence and best guesses
74
+
75
+ Nothing here was observed against a live Fitbit account (the legacy API has no sandbox and needs a consenting user). The contract follows the vendor reference at <https://dev.fitbit.com/build/reference/web-api/> (authorize, token, refresh, revoke, activity list, sleep by date, profile). Everything below is a **best guess, not oracle-verified** and is tracked in the issue as `needs-oracle-check`:
76
+
77
+ - the `{"errors":[{"errorType","message","fieldName"?}],"success":false}` envelope and the `errorType` names (`invalid_client`, `invalid_grant`, `invalid_request`, `unsupported_grant_type`, `invalid_token`, `expired_token`, `insufficient_scope`, `insufficient_permissions`, `validation`, `too_many_requests`), and their messages and statuses (401 for bad client credentials and bad or expired bearer tokens, 400 for bad grants and parameters, 403 for scope or another user's id);
78
+ - single-use refresh rotation killing the previous access token, and not consuming an authorization code on a failed exchange;
79
+ - `offset` above 0 being honored (the reference documents `Supported: 0` and builds `next` for the following page), `next` being an empty string on the last page, and `next` using the next `offset` rather than a moved `afterDate`;
80
+ - the `afterDate`/`sort` pairing rules and the `limit` 1 to 100 bound being enforced as 400 `validation`;
81
+ - `/1/` and `/1.2/` sleep serving identical data, including `levels.summary` stage totals;
82
+ - revoke answering 200 `{}` and an unknown token also 200 (RFC 7009), an unknown user id in the path answering 403;
83
+ - the 429 body and header set, and the defaults for profile, goals and an empty day.
84
+
85
+ ## Oracle and tests
86
+
87
+ `bun test` runs the acceptance tests (one per numbered behavior of the service request, through `test/consumer.ts`, a port of the consumer's httpx client) and every-operation self-parity with divergence detection. `bun run parity` needs `FITBIT_ACCESS_TOKEN`, performs safe read-only profile and activity-list envelope checks, and never prints health data or credentials.
88
+
89
+ ## Deliberately not modelled
90
+
91
+ Intraday time series, heart-rate and SpO2 time series, devices, friends, subscriptions and webhooks, writes (create log, goals), the implicit and client-credentials grants, public (secretless) PKCE clients, `GET /1/user/-/activities/{id}.json` (not a documented per-user route), the TCX download, the Google Health API, a consent UI (consent is decided by the `consent` control and the browser is only redirected), the `#_=_` fragment the vendor appends to the redirect, rate-limit accounting (limits are only scripted through presets), real health data and device pairing.
92
+
93
+ ## API
94
+
95
+ Root runtime exports: `FitbitAPI`, `FITBIT_NAMESPACE`, `FITBIT_SCOPES`, `ACCESS_TOKEN_SECONDS`, `CODE_SECONDS`, `fitbitError`, `createRuntime`, `FITBIT_PRESETS`, `document`, `operationIds`, `supportedOperationIds`. Types include `FitbitAPIOptions`, `Client`, `User`, `Grant`, `OAuthCode`, `ActivityLog`, `ActivityLogEntry`, `SleepLog`, `SleepLogEntry`, `DailySummary`, `Consent`, `FitbitRuntimeOptions`, `FitbitRuntime`, `OperationId`, `SupportedOperationId`.
96
+
97
+ `/server` exports `createServer`, `serveTarget`, `DEFAULT_PORT` and types `FitbitServerOptions`, `FitbitServer`.
package/SUPPORT.md ADDED
@@ -0,0 +1,18 @@
1
+ # Fitbit legacy Web API emulator — operation support
2
+
3
+ Generated from `openapi.yaml`; do not edit by hand.
4
+
5
+ - operations in spec: **8**
6
+ - supported by the emulator: **8**
7
+ - parity enabled: **7**
8
+
9
+ | operationId | route | emulator | parity | notes |
10
+ | --- | --- | --- | --- | --- |
11
+ | `Authorize` | `GET /oauth2/authorize` | ✅ supported | ❌ disabled | Interactive consent redirects are exercised by the acceptance tests through the consumer. |
12
+ | `OAuthToken` | `POST /oauth2/token` | ✅ supported | ⚠️ unsafe (opt-in) | |
13
+ | `RevokeToken` | `POST /oauth2/revoke` | ✅ supported | ⚠️ unsafe (opt-in) | |
14
+ | `GetProfile` | `GET /1/user/{userId}/profile.json` | ✅ supported | ✅ | |
15
+ | `ListActivityLogs` | `GET /1/user/{userId}/activities/list.json` | ✅ supported | ✅ | |
16
+ | `GetDailyActivitySummary` | `GET /1/user/{userId}/activities/date/{date}.json` | ✅ supported | ✅ | |
17
+ | `GetSleepByDate` | `GET /1/user/{userId}/sleep/date/{date}.json` | ✅ supported | ✅ | |
18
+ | `GetSleepByDateV12` | `GET /1.2/user/{userId}/sleep/date/{date}.json` | ✅ supported | ✅ | |