@crvouga/mockingbird-service-polar 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-polar
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-polar 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-polar/`; 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 code exchange, user registration and exercise windows**.
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 -- polar`.
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: `polar`.
package/README.md CHANGED
@@ -1,3 +1,104 @@
1
- # Temporary Holding Version
1
+ # @crvouga/mockingbird-service-polar
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 Polar AccessLink v3 emulator: OAuth 2.0 authorization code, user registration and the exercise endpoints, with the 30-day visibility window.
6
+
7
+ ## Install
8
+
9
+ `bun add @crvouga/mockingbird-service-polar`
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { createServer } from "@crvouga/mockingbird-service-polar/server"
15
+
16
+ // Test setup: a local Polar. The user is registered with the client ten days ago and has an
17
+ // exercise uploaded afterwards. Seeds, presets and the clock are the only emulator-specific lines.
18
+ const now = Date.now()
19
+ const server = await createServer({
20
+ registrations: [
21
+ { xUserId: 1001, memberId: "m-1", registeredAt: new Date(now - 10 * 86_400_000).toISOString() },
22
+ ],
23
+ exercises: [
24
+ {
25
+ xUserId: 1001,
26
+ id: "2AC312F",
27
+ upload_time: new Date(now - 86_400_000).toISOString(),
28
+ start_time: "2026-01-30T08:00:00",
29
+ duration: "PT2H44M",
30
+ },
31
+ ],
32
+ })
33
+
34
+ // Application code: the same three origins, each pointed at the emulator.
35
+ const apiOrigin = server.url
36
+ const tokenUrl = `${server.url}/v2/oauth2/token`
37
+ const authorizeUrl = `${server.url}/oauth2/authorization`
38
+
39
+ const consent = await fetch(
40
+ `${authorizeUrl}?response_type=code&client_id=mock_client&redirect_uri=https://app.example.com/cb&state=s1&scope=accesslink.read_all`,
41
+ { redirect: "manual" },
42
+ )
43
+ const code = new URL(consent.headers.get("location") ?? "").searchParams.get("code") ?? ""
44
+ const tokenResponse = await fetch(tokenUrl, {
45
+ method: "POST",
46
+ headers: {
47
+ "content-type": "application/x-www-form-urlencoded",
48
+ authorization: `Basic ${btoa("mock_client:mock_client_secret")}`,
49
+ },
50
+ body: new URLSearchParams({
51
+ grant_type: "authorization_code",
52
+ code,
53
+ redirect_uri: "https://app.example.com/cb",
54
+ }),
55
+ })
56
+ const { access_token } = (await tokenResponse.json()) as { access_token: string }
57
+ const exercises = await fetch(`${apiOrigin}/v3/exercises?samples=false&zones=false&route=false`, {
58
+ headers: { authorization: `Bearer ${access_token}`, accept: "application/json" },
59
+ })
60
+ console.log(await exercises.json())
61
+
62
+ await server.close()
63
+ ```
64
+
65
+ Run `mockingbird-polar serve --port 12157`. The consumer has three origin settings: set the API origin, the token URL (`<origin>/v2/oauth2/token`) and the authorize URL (`<origin>/oauth2/authorization`) to the emulator. One emulator serves all three route sets, so they share the origin. The consent page is a browser redirect, so a browser cannot send `x-mockingbird-namespace`: use the `/__admin/ns/<name>` prefix (`<origin>/__admin/ns/<name>/oauth2/authorization`) to pick a namespace for it.
66
+
67
+ ## Routes and state
68
+
69
+ Defaults: client `mock_client` / `mock_client_secret` (any redirect URI unless the client lists `redirectUris`), user `x_user_id` 1001 (not registered), and the access token `mock_polar_token` for that user. `GET /oauth2/authorization` redirects to `redirect_uri?code=<code>&state=<state>` as the first (or `consentUser`) user; the code is one-time and good for ten minutes on the emulator clock. `POST /v2/oauth2/token` takes a form body and Basic client auth and answers `{access_token, token_type: "bearer", expires_in: 31535999, x_user_id}` (integers; no `refresh_token`, no `scope`). Tokens never expire; they stop working only when revoked. A second use of a code, a different client's code or a mismatched `redirect_uri` is 400 `invalid_grant`; an unknown client or wrong secret is 400 `invalid_client`; a non-form body, a missing `grant_type`, or any grant other than `authorization_code` (including `refresh_token`) is 400 `invalid_request`.
70
+
71
+ `POST /v3/users` with `{"member-id": "..."}` registers the token's user with the token's client at the emulator clock's instant and answers 200 with the profile (`polar-user-id`, `member-id`, `registration-date`, `first-name`, ...). A user already registered, or a `member-id` another user of that client holds, is 409. A body without `member-id` is 400; a non-JSON body is 415.
72
+
73
+ `GET /v3/exercises` returns a JSON array of the token user's exercises that were uploaded at or after registration and no more than 30 days before the emulator clock (an exact 30 days is still returned), oldest upload first; `[]` when there are none. `GET /v3/exercises/{id}` returns one, or 404 for an unknown id, another user's exercise, or one outside that window. `samples`, `zones` and `route` default to false and must read `true` or `false` (else 400); when true the item gains `samples`, `heart_rate_zones` or `route` (from the seed, `[]` when none). A token whose user is not registered with the client gets 403 on both. Items always carry `id`, `upload_time`, `polar_user`, `device`, `sport`, `start_time`, `start_time_utc_offset` and `duration`; a seed that omits `device`, `device_id`, `sport`, `detailed_sport_info`, `start_time_utc_offset`, `duration`, `calories`, `distance`, `heart_rate` or `has_route` gets synthetic defaults (a Polar Vantage V, a 30 minute run).
74
+
75
+ Every API route answers a missing, unknown or revoked token with 401.
76
+
77
+ ## Test controls
78
+
79
+ Seed through options (`clients`, `users`, `registrations`, `exercises`, `tokens`, `codes`, `settings`) or the admin routes below; all state shows under `GET /__admin/state`. Admin routes (relocatable with the admin prefix):
80
+
81
+ - `GET|PUT /__admin/settings`: `{consent: "approve" | "deny" | "error", consentUser: <x_user_id> | null}`. `deny` redirects with `error=access_denied`, `error` with `error=server_error`, both carrying the `state`.
82
+ - `POST /__admin/users` `{xUserId, firstName?, lastName?, birthdate?, gender?, weight?, height?}`.
83
+ - `POST /__admin/registrations` `{xUserId, memberId, registeredAt, clientId?}` (ISO 8601).
84
+ - `POST /__admin/exercises` `{xUserId, id, upload_time, start_time, ...fields}`. Seed one uploaded before registration and one older than 30 days to see them excluded.
85
+ - `POST /__admin/tokens/revoke` `{token}`: that token answers 401 from then on.
86
+ - The clock (`POST /__admin/clock`) moves the 30-day window and the code lifetime.
87
+
88
+ Presets (`POST /__admin/faults {"preset": ..., "count": n}`): `unauthorized` (401), `rate_limited` (429 with `RateLimit-Usage`, `RateLimit-Limit`, `RateLimit-Reset`, no `Retry-After`), `rate_limited_retry_after` (the same plus `Retry-After: 5`), `server_error` (500) and `connection_drop`. Generic faults add any other status, headers or latency. Header namespaces (`x-mockingbird-namespace`), `/__admin/ns/<name>` paths and bearer-token mappings (`PUT /__admin/credentials`) isolate users, registrations, exercises and tokens. Journals record metadata, not bodies. No webhooks.
89
+
90
+ ## Oracle and tests
91
+
92
+ Derived from the [AccessLink reference](https://www.polar.com/accesslink-api/) and the consumer's wire shapes (issue #546). **No live credentials were used; nothing here is oracle-verified.** The following are a best guess, not oracle-verified: the `invalid_request` error for `grant_type=refresh_token`; 403 for an unregistered user's exercise calls; 200 and `[]` for a registered user with no exercises; 404 for an unknown exercise id; the JSON shape of API error bodies (`{"error": {"timestamp", "status", "errorType", "message"}}`); redirect-based `invalid_scope`, `unsupported_response_type` and `server_error` on the consent page; the ten-minute code lifetime; omitting `redirect_uri` at the token endpoint being accepted; the `RateLimit-*` values on the 429 presets (the documented default limits of 500 per 15 minutes and 5000 per 24 hours, exhausted) and no `Retry-After` on the default 429 (`rate_limited_retry_after` is the variant). Successful responses never carry `RateLimit-*` headers.
93
+
94
+ `bun test` runs the acceptance tests (every numbered behavior of the request, through `test/consumer.ts`), all-operation self-parity (the consent page is a browser redirect and is excluded from random walks) and divergence detection. `bun run parity` requires `POLAR_ACCESS_TOKEN` of an already registered user and performs a read-only exercise list envelope check without printing data.
95
+
96
+ ## Deliberately not modelled
97
+
98
+ The deprecated transaction API (create transaction, commit), daily activity, continuous heart rate, cardio load, sleep, Nightly Recharge, SleepWise and Elixir data, FIT/TCX/GPX downloads, webhooks, user deletion and the v4 Dynamic API. Also: a real login or consent UI (the consent page decides from the `consent` setting), Polar Flow accounts and devices, rate-limit accounting (the `RateLimit-*` headers appear only on forced 429s), the `training_load`, `fat_percentage`-style optional exercise fields (seed them if needed; they pass through), and token expiry (`expires_in` is a constant).
99
+
100
+ ## API
101
+
102
+ Root runtime exports: `PolarAPI`, `POLAR_NAMESPACE`, `POLAR_SCOPE`, `EXPIRES_IN`, `EXERCISE_WINDOW_MS`, `CODE_TTL_MS`, `DEFAULT_SETTINGS`, `createRuntime`, `POLAR_PRESETS`, `document`, `operationIds`, `supportedOperationIds`. Types: `PolarAPIOptions`, `Client`, `User`, `RegistrationSeed`, `Registration`, `TokenGrant`, `OAuthCode`, `ExerciseSeed`, `ExerciseRow`, `ConsentMode`, `Settings`, `PolarRuntimeOptions`, `PolarRuntime`, `OperationId`, `SupportedOperationId`.
103
+
104
+ `/server`: `createServer`, `serveTarget`, `DEFAULT_PORT`; types `PolarServerOptions`, `PolarServer`.
package/SUPPORT.md ADDED
@@ -0,0 +1,15 @@
1
+ # Polar AccessLink API v3 (Mockingbird subset) — operation support
2
+
3
+ Generated from `openapi.yaml`; do not edit by hand.
4
+
5
+ - operations in spec: **5**
6
+ - supported by the emulator: **5**
7
+ - parity enabled: **4**
8
+
9
+ | operationId | route | emulator | parity | notes |
10
+ | --- | --- | --- | --- | --- |
11
+ | `Authorize` | `GET /oauth2/authorization` | ✅ supported | ❌ disabled | Browser redirect flow; covered by acceptance tests. |
12
+ | `OAuthToken` | `POST /v2/oauth2/token` | ✅ supported | ⚠️ unsafe (opt-in) | |
13
+ | `RegisterUser` | `POST /v3/users` | ✅ supported | ⚠️ unsafe (opt-in) | |
14
+ | `ListExercises` | `GET /v3/exercises` | ✅ supported | ✅ | |
15
+ | `GetExercise` | `GET /v3/exercises/{id}` | ✅ supported | ✅ | |