@molecule/api-wearable-fitbit 1.0.0 → 1.0.2

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.
Files changed (2) hide show
  1. package/README.md +749 -0
  2. package/package.json +12 -11
package/README.md ADDED
@@ -0,0 +1,749 @@
1
+ <!--
2
+ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
+ Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
+ Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
+ To change this document, edit the module-level JSDoc in src/index.ts.
6
+ Generated: 2026-08-04T01:49:46.873Z
7
+ -->
8
+
9
+ # @molecule/api-wearable-fitbit
10
+
11
+ > **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
12
+ > It is written to be read by coding agents as much as by people, and is generated from this
13
+ > package's source — edit `src/index.ts` JSDoc, not this file.
14
+
15
+ Fitbit Web API bond for `@molecule/api-wearable`.
16
+
17
+ Implements daily activity, sleep, heart-rate, and weight ingestion
18
+ against the Fitbit Web API using OAuth 2.0 PKCE with refresh-token
19
+ rotation. Wires under the `wearable` named-multi-provider category as
20
+ `'fitbit'`.
21
+
22
+ ## Quick Start
23
+
24
+ ```typescript
25
+ import { setProvider } from '@molecule/api-wearable'
26
+ import { createProvider, PROVIDER_NAME } from '@molecule/api-wearable-fitbit'
27
+
28
+ const fitbit = createProvider({
29
+ redirectUri: 'https://app.example.com/auth/fitbit/callback',
30
+ credentialsStore: myCredentialsStore,
31
+ codeVerifierStore: myVerifierStore,
32
+ })
33
+
34
+ setProvider(PROVIDER_NAME, fitbit)
35
+ ```
36
+
37
+ ## Type
38
+
39
+ `provider`
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ npm install @molecule/api-wearable-fitbit @molecule/api-bond @molecule/api-http @molecule/api-secrets @molecule/api-wearable
45
+ ```
46
+
47
+ ## API
48
+
49
+ ### Interfaces
50
+
51
+ #### `DailyActivity`
52
+
53
+ Daily activity rollup. All fields are zero-defaulted by the bond when
54
+ the provider does not return a value, so handlers can sum across days
55
+ without null-checks.
56
+
57
+ ```typescript
58
+ interface DailyActivity {
59
+ /** Calendar day this rollup covers (`YYYY-MM-DD`). */
60
+ date: WearableDate
61
+ /** Total step count for the day. */
62
+ steps: number
63
+ /** Total distance traveled, in meters. */
64
+ distanceMeters: number
65
+ /** Total calories burned (active + BMR), in kilocalories. */
66
+ caloriesOut: number
67
+ /** Active minutes (provider-specific intensity definitions). */
68
+ activeMinutes: number
69
+ /** Floors climbed, when reported. */
70
+ floors?: number
71
+ /** Total elevation gain, in meters, when reported. */
72
+ elevationMeters?: number
73
+ /** Resting heart-rate, in beats per minute, when reported. */
74
+ restingHeartRate?: number
75
+ }
76
+ ```
77
+
78
+ #### `FitbitAuthorizeStart`
79
+
80
+ Result of {@link FitbitProvider.startAuthorize} — the URL the host
81
+ should redirect the user to plus the `state` value to round-trip.
82
+
83
+ ```typescript
84
+ interface FitbitAuthorizeStart {
85
+ /** Fitbit OAuth authorize URL. */
86
+ url: string
87
+ /** Opaque `state` value the host MUST verify on the callback. */
88
+ state: string
89
+ }
90
+ ```
91
+
92
+ #### `FitbitCodeVerifierStore`
93
+
94
+ Cache of `code_verifier` strings keyed by the in-flight authorization
95
+ `code` string they were paired with.
96
+
97
+ The Fitbit OAuth callback hands the bond an authorization `code` but
98
+ not the `code_verifier` it was created with — the bond needs to store
99
+ the verifier between {@link FitbitProvider.startAuthorize} and
100
+ {@link FitbitProvider.connect}. The store contract leaves the choice
101
+ of backing storage (Redis, Postgres, in-memory for tests) to the host.
102
+
103
+ ```typescript
104
+ interface FitbitCodeVerifierStore {
105
+ /**
106
+ * Persists the verifier indexed by the `state` value the bond will
107
+ * round-trip through Fitbit's authorize redirect.
108
+ *
109
+ * @param state - Opaque CSRF-protection token returned in the redirect.
110
+ * @param verifier - The PKCE `code_verifier` to retrieve later.
111
+ */
112
+ put(state: string, verifier: string): Promise<void>
113
+ /**
114
+ * Retrieves and deletes the verifier associated with `state`.
115
+ * Implementations MUST delete on read so a verifier is never reused.
116
+ *
117
+ * @param state - The `state` value extracted from the OAuth callback.
118
+ * @returns The verifier, or `null` if no entry exists.
119
+ */
120
+ take(state: string): Promise<string | null>
121
+ }
122
+ ```
123
+
124
+ #### `FitbitProvider`
125
+
126
+ Public surface returned by {@link createProvider}. Extends the
127
+ stack-neutral {@link import('@molecule/api-wearable').WearableProvider}
128
+ with Fitbit-specific OAuth-flow helpers.
129
+
130
+ ```typescript
131
+ interface FitbitProvider extends WearableProviderType {
132
+ /**
133
+ * Builds the Fitbit authorize URL and persists a fresh PKCE verifier.
134
+ *
135
+ * @returns The URL to redirect to and the round-trip `state` value.
136
+ */
137
+ startAuthorize(): Promise<FitbitAuthorizeStart>
138
+ /**
139
+ * Performs the authorization-code → token exchange with an explicit
140
+ * PKCE `code_verifier`. Use this when the host wires the OAuth flow
141
+ * itself (no `codeVerifierStore` configured).
142
+ *
143
+ * @param userId - Host-app user identifier.
144
+ * @param code - OAuth authorization `code` from the redirect callback.
145
+ * @param verifier - The PKCE `code_verifier` used to derive the challenge.
146
+ * @returns The freshly-minted user connection (already persisted).
147
+ */
148
+ connectWithVerifier(
149
+ userId: string,
150
+ code: string,
151
+ verifier: string,
152
+ ): Promise<WearableUserConnection>
153
+ }
154
+ ```
155
+
156
+ #### `FitbitProviderOptions`
157
+
158
+ Configuration options for {@link createProvider}.
159
+
160
+ ```typescript
161
+ interface FitbitProviderOptions {
162
+ /**
163
+ * OAuth client id. Defaults to `process.env.OAUTH_FITBIT_CLIENT_ID`.
164
+ */
165
+ clientId?: string
166
+ /**
167
+ * OAuth client secret. Defaults to
168
+ * `process.env.OAUTH_FITBIT_CLIENT_SECRET`. Optional — PKCE-only public
169
+ * clients omit it.
170
+ */
171
+ clientSecret?: string
172
+ /**
173
+ * OAuth redirect URI registered with the Fitbit application. Required
174
+ * to complete the authorization-code exchange.
175
+ */
176
+ redirectUri: string
177
+ /**
178
+ * Persists user connections (access + refresh tokens). Required.
179
+ */
180
+ credentialsStore: WearableCredentialsStore
181
+ /**
182
+ * Optional store for PKCE `code_verifier` strings keyed by `state`.
183
+ * Required if the bond's `startAuthorize` / `connect` helpers are used
184
+ * to drive the OAuth flow; the host may omit it when wiring the OAuth
185
+ * exchange themselves.
186
+ */
187
+ codeVerifierStore?: FitbitCodeVerifierStore
188
+ /**
189
+ * OAuth scopes to request when starting authorization. Defaults to the
190
+ * union needed for activity, sleep, heart-rate, and weight reads.
191
+ */
192
+ scopes?: readonly string[]
193
+ /**
194
+ * Override the Fitbit Web API base URL. Defaults to
195
+ * `https://api.fitbit.com/1`.
196
+ */
197
+ apiBaseUrl?: string
198
+ /**
199
+ * Override the Fitbit OAuth authorize endpoint. Defaults to
200
+ * `https://www.fitbit.com/oauth2/authorize`.
201
+ */
202
+ authorizeUrl?: string
203
+ /**
204
+ * Override the Fitbit OAuth token endpoint. Defaults to
205
+ * `https://api.fitbit.com/oauth2/token`.
206
+ */
207
+ tokenUrl?: string
208
+ /**
209
+ * Override the Fitbit OAuth token-revoke endpoint. Defaults to
210
+ * `https://api.fitbit.com/oauth2/revoke`.
211
+ */
212
+ revokeUrl?: string
213
+ /**
214
+ * Request timeout in milliseconds. Defaults to `15_000`.
215
+ */
216
+ timeoutMs?: number
217
+ /**
218
+ * Optional override for the random-bytes generator used by PKCE / CSRF.
219
+ * Tests inject a deterministic generator.
220
+ */
221
+ randomBytes?: (size: number) => Uint8Array
222
+ /**
223
+ * Optional clock override, primarily for tests. Defaults to `Date.now`.
224
+ */
225
+ now?: () => number
226
+ }
227
+ ```
228
+
229
+ #### `HeartRateSummary`
230
+
231
+ Daily heart-rate summary.
232
+
233
+ ```typescript
234
+ interface HeartRateSummary {
235
+ /** Calendar day this summary covers (`YYYY-MM-DD`). */
236
+ date: WearableDate
237
+ /** Resting heart-rate, in beats per minute, when reported. */
238
+ restingHeartRate?: number
239
+ /** Per-zone breakdown, when reported. */
240
+ zones?: HeartRateZone[]
241
+ }
242
+ ```
243
+
244
+ #### `HeartRateZone`
245
+
246
+ Heart-rate zone definition (rest/fat-burn/cardio/peak or provider-named).
247
+
248
+ ```typescript
249
+ interface HeartRateZone {
250
+ /** Zone label as reported by the provider. */
251
+ name: string
252
+ /** Lower bound of the zone, in beats per minute (inclusive). */
253
+ minBpm: number
254
+ /** Upper bound of the zone, in beats per minute (exclusive). */
255
+ maxBpm: number
256
+ /** Minutes spent in the zone for this period. */
257
+ minutes: number
258
+ /** Calories burned in the zone, in kilocalories. */
259
+ caloriesOut?: number
260
+ }
261
+ ```
262
+
263
+ #### `SleepSession`
264
+
265
+ One sleep session. Most users have exactly one per night, but providers
266
+ report naps as separate sessions, so handlers must sum across the array.
267
+
268
+ ```typescript
269
+ interface SleepSession {
270
+ /** Provider-specific session id. */
271
+ id: string
272
+ /** Calendar day this session is bucketed under (`YYYY-MM-DD`). */
273
+ date: WearableDate
274
+ /** ISO 8601 sleep start. */
275
+ start: string
276
+ /** ISO 8601 sleep end. */
277
+ end: string
278
+ /** Total time in bed, in minutes. */
279
+ timeInBedMinutes: number
280
+ /** Total time asleep, in minutes (excludes `awake` segments). */
281
+ timeAsleepMinutes: number
282
+ /** Sleep efficiency percentage (0-100). */
283
+ efficiency?: number
284
+ /** Whether this session is the user's primary sleep for the day. */
285
+ isMainSleep: boolean
286
+ /** Per-stage minute totals when the provider reports stages. */
287
+ stageSummary?: SleepStageSummary
288
+ /** Per-segment stage breakdown when the provider reports stages. */
289
+ segments?: SleepStageSegment[]
290
+ }
291
+ ```
292
+
293
+ #### `SleepStageSegment`
294
+
295
+ A contiguous block of a single sleep stage within a sleep session.
296
+
297
+ ```typescript
298
+ interface SleepStageSegment {
299
+ /** Normalized stage. */
300
+ stage: SleepStage
301
+ /** ISO 8601 segment start. */
302
+ start: string
303
+ /** ISO 8601 segment end. */
304
+ end: string
305
+ /** Segment duration in seconds (provider-reported when available). */
306
+ durationSeconds: number
307
+ }
308
+ ```
309
+
310
+ #### `SleepStageSummary`
311
+
312
+ Per-stage totals for a sleep session, in minutes. Optional — providers
313
+ that only report the coarse "asleep" classification will omit
314
+ `light`/`deep`/`rem`.
315
+
316
+ ```typescript
317
+ interface SleepStageSummary {
318
+ /** Minutes spent awake during the session. */
319
+ awakeMinutes?: number
320
+ /** Minutes in light sleep. */
321
+ lightMinutes?: number
322
+ /** Minutes in deep sleep. */
323
+ deepMinutes?: number
324
+ /** Minutes in REM sleep. */
325
+ remMinutes?: number
326
+ /** Minutes restless (legacy classifications). */
327
+ restlessMinutes?: number
328
+ }
329
+ ```
330
+
331
+ #### `UserConnection`
332
+
333
+ Per-user OAuth tokens minted by {@link WearableProvider.connect} and
334
+ rotated by {@link WearableProvider.refreshConnection}. Bonds persist
335
+ these via a caller-supplied {@link WearableCredentialsStore}.
336
+
337
+ Token strings are NEVER thrown, logged, or echoed back in error
338
+ messages — implementations must sanitize all error paths.
339
+
340
+ ```typescript
341
+ interface UserConnection {
342
+ /** The user owning the connection in the host application. */
343
+ userId: string
344
+ /** Provider-specific account identifier (e.g. Fitbit `user_id`). */
345
+ providerAccountId: string
346
+ /** Current access token. */
347
+ accessToken: string
348
+ /** Refresh token used to mint new access tokens. */
349
+ refreshToken: string
350
+ /** Optional epoch-millis timestamp at which the access token expires. */
351
+ expiresAt?: number
352
+ /** Granted OAuth scopes (provider-specific names). */
353
+ scopes?: string[]
354
+ /** Epoch-millis timestamp at which the connection was first established. */
355
+ connectedAt: number
356
+ }
357
+ ```
358
+
359
+ #### `WearableCredentialsStore`
360
+
361
+ Persistence contract for {@link UserConnection} records. Implementations
362
+ are responsible for at-rest encryption (refresh tokens are bearer
363
+ credentials and MUST be stored securely).
364
+
365
+ The same store can back any number of provider bonds (Fitbit, Oura,
366
+ Withings, etc.) — segregation is by `(userId, providerName)` pair.
367
+
368
+ ```typescript
369
+ interface WearableCredentialsStore {
370
+ /**
371
+ * Looks up the connection for `(userId, providerName)`.
372
+ *
373
+ * @param userId - Host-app user identifier.
374
+ * @param providerName - Provider key, e.g. `"fitbit"`.
375
+ * @returns The stored connection or `null` if none.
376
+ */
377
+ read(userId: string, providerName: string): Promise<UserConnection | null>
378
+ /**
379
+ * Persists a new or refreshed connection.
380
+ *
381
+ * @param providerName - Provider key, e.g. `"fitbit"`.
382
+ * @param connection - The connection record to write.
383
+ */
384
+ write(providerName: string, connection: UserConnection): Promise<void>
385
+ /**
386
+ * Deletes the connection for `(userId, providerName)`.
387
+ *
388
+ * @param userId - Host-app user identifier.
389
+ * @param providerName - Provider key.
390
+ */
391
+ remove(userId: string, providerName: string): Promise<void>
392
+ }
393
+ ```
394
+
395
+ #### `WearableDateRange`
396
+
397
+ Inclusive date range.
398
+
399
+ ```typescript
400
+ interface WearableDateRange {
401
+ /** Lower-bound calendar day (`YYYY-MM-DD`), inclusive. */
402
+ start: WearableDate
403
+ /** Upper-bound calendar day (`YYYY-MM-DD`), inclusive. */
404
+ end: WearableDate
405
+ }
406
+ ```
407
+
408
+ #### `WearableProvider`
409
+
410
+ Wearable provider contract.
411
+
412
+ Each method takes a host-app `userId` rather than per-call credentials.
413
+ Implementations look up persisted {@link UserConnection} records via the
414
+ caller-supplied {@link WearableCredentialsStore}, transparently refresh
415
+ expired access tokens (writing the rotated record back to the store),
416
+ and surface refreshes as a side-effect of normal data calls.
417
+
418
+ Token-bearing values (access tokens, refresh tokens, Authorization
419
+ headers, OAuth `error_description` payloads that may include the token)
420
+ MUST NEVER be thrown, logged, or returned in error messages — every
421
+ error path must be sanitized.
422
+
423
+ ```typescript
424
+ interface WearableProvider {
425
+ /** Stable provider key used for credential-store segregation, e.g. `"fitbit"`. */
426
+ readonly providerName: string
427
+ /**
428
+ * Reads the daily activity rollup for `(userId, date)`.
429
+ *
430
+ * @param userId - Host-app user identifier.
431
+ * @param date - Calendar day (`YYYY-MM-DD`).
432
+ * @returns Normalized daily activity.
433
+ */
434
+ getDailyActivity(userId: string, date: WearableDate): Promise<DailyActivity>
435
+ /**
436
+ * Reads sleep sessions bucketed under `(userId, date)`. Most days return
437
+ * a single primary session; multi-nap days return multiple.
438
+ *
439
+ * @param userId - Host-app user identifier.
440
+ * @param date - Calendar day (`YYYY-MM-DD`).
441
+ * @returns All sleep sessions bucketed under that day.
442
+ */
443
+ getDailySleep(userId: string, date: WearableDate): Promise<SleepSession[]>
444
+ /**
445
+ * Reads the daily heart-rate summary for `(userId, date)`.
446
+ *
447
+ * @param userId - Host-app user identifier.
448
+ * @param date - Calendar day (`YYYY-MM-DD`).
449
+ * @returns Normalized daily heart-rate summary.
450
+ */
451
+ getDailyHeartRate(userId: string, date: WearableDate): Promise<HeartRateSummary>
452
+ /**
453
+ * Reads body-weight entries for the user across `range`. Providers cap
454
+ * how far back a single call may reach — implementations should clamp
455
+ * `range` to the provider's documented maximum and return only the
456
+ * available entries.
457
+ *
458
+ * @param userId - Host-app user identifier.
459
+ * @param range - Inclusive calendar-day range.
460
+ * @returns Weight entries, ordered by `recordedAt` ascending.
461
+ */
462
+ getWeight(userId: string, range: WearableDateRange): Promise<WeightEntry[]>
463
+ /**
464
+ * Exchanges an OAuth authorization `code` for tokens and persists the
465
+ * resulting {@link UserConnection} via the bond's credentials store.
466
+ *
467
+ * @param userId - Host-app user identifier (the local user accepting the link).
468
+ * @param code - Authorization code from the OAuth redirect callback.
469
+ * @returns The freshly-minted connection (already persisted).
470
+ */
471
+ connect(userId: string, code: string): Promise<UserConnection>
472
+ /**
473
+ * Forces a refresh of the user's access token using the stored refresh
474
+ * token. The rotated connection is persisted before being returned.
475
+ *
476
+ * @param userId - Host-app user identifier.
477
+ * @returns The rotated connection (already persisted).
478
+ */
479
+ refreshConnection(userId: string): Promise<UserConnection>
480
+ /**
481
+ * Revokes (best-effort) and removes the user's connection record.
482
+ *
483
+ * Implementations SHOULD attempt to revoke the token at the provider but
484
+ * MUST always remove the local record even if revocation fails — leaking
485
+ * a record after `disconnect` is worse than a stranded provider-side
486
+ * token.
487
+ *
488
+ * @param userId - Host-app user identifier.
489
+ */
490
+ disconnect(userId: string): Promise<void>
491
+ }
492
+ ```
493
+
494
+ #### `WeightEntry`
495
+
496
+ One body-weight reading.
497
+
498
+ ```typescript
499
+ interface WeightEntry {
500
+ /** ISO 8601 timestamp at which the reading was taken. */
501
+ recordedAt: string
502
+ /** Calendar day of the reading (`YYYY-MM-DD`). */
503
+ date: WearableDate
504
+ /** Weight in kilograms. */
505
+ weightKg: number
506
+ /** Body-fat percentage (0-100), when reported. */
507
+ bodyFatPercent?: number
508
+ /** BMI (kg / m²), when reported. */
509
+ bmi?: number
510
+ /** Provider-specific entry id. */
511
+ id?: string
512
+ }
513
+ ```
514
+
515
+ ### Types
516
+
517
+ #### `SleepStage`
518
+
519
+ Sleep stage taxonomy normalized across providers.
520
+
521
+ - `awake` — periods awake during a sleep session
522
+ - `light`, `deep`, `rem` — modern stage classifications
523
+ - `restless`, `asleep` — legacy/coarse classifications used by some providers
524
+ - `unknown` — fallback for unmapped values
525
+
526
+ ```typescript
527
+ type SleepStage = 'awake' | 'light' | 'deep' | 'rem' | 'restless' | 'asleep' | 'unknown'
528
+ ```
529
+
530
+ #### `WearableDate`
531
+
532
+ Calendar-day identifier in `YYYY-MM-DD` format. Wearable providers
533
+ universally bucket activity/sleep/HR data by local-day, so the core
534
+ exchanges date strings (not absolute timestamps) for per-day reads.
535
+
536
+ ```typescript
537
+ type WearableDate = string
538
+ ```
539
+
540
+ ### Functions
541
+
542
+ #### `base64UrlEncode(bytes)`
543
+
544
+ Base64url-encodes the bytes (RFC 4648 §5 with `=` padding stripped).
545
+
546
+ ```typescript
547
+ function base64UrlEncode(bytes: Uint8Array<ArrayBufferLike>): string
548
+ ```
549
+
550
+ - `bytes` — The bytes to encode.
551
+
552
+ **Returns:** A base64url string.
553
+
554
+ #### `createProvider(options)`
555
+
556
+ Creates a Fitbit wearable provider.
557
+
558
+ ```typescript
559
+ function createProvider(options: FitbitProviderOptions): FitbitProvider
560
+ ```
561
+
562
+ - `options` — Required: `redirectUri` + `credentialsStore`. Falls back to `OAUTH_FITBIT_CLIENT_ID` / `OAUTH_FITBIT_CLIENT_SECRET` env vars when `clientId` / `clientSecret` are omitted.
563
+
564
+ **Returns:** A Fitbit-flavored {@link FitbitProvider}.
565
+
566
+ #### `fromFitbitActivity(date, raw)`
567
+
568
+ Translates a Fitbit `activities/date` summary payload into the
569
+ normalized {@link DailyActivity} shape. Missing fields are zero-defaulted
570
+ so handlers can sum across days without null guards.
571
+
572
+ ```typescript
573
+ function fromFitbitActivity(date: string, raw: FitbitDailyActivityResponse): DailyActivity
574
+ ```
575
+
576
+ - `date` — The calendar day the response was requested for.
577
+ - `raw` — Fitbit's daily-activity response payload.
578
+
579
+ **Returns:** A normalized daily activity record.
580
+
581
+ #### `fromFitbitHeartRate(date, raw)`
582
+
583
+ Translates a Fitbit `activities/heart/date` payload into the normalized
584
+ {@link HeartRateSummary} shape.
585
+
586
+ ```typescript
587
+ function fromFitbitHeartRate(date: string, raw: FitbitHeartRateResponse): HeartRateSummary
588
+ ```
589
+
590
+ - `date` — Calendar day the response was requested for.
591
+ - `raw` — Fitbit's heart-rate response payload.
592
+
593
+ **Returns:** A normalized heart-rate summary.
594
+
595
+ #### `fromFitbitSleep(date, raw)`
596
+
597
+ Translates a Fitbit `sleep/date` payload into a list of normalized
598
+ {@link SleepSession} records.
599
+
600
+ ```typescript
601
+ function fromFitbitSleep(date: string, raw: FitbitSleepResponse): SleepSession[]
602
+ ```
603
+
604
+ - `date` — Calendar day the response was requested for.
605
+ - `raw` — Fitbit's sleep response payload.
606
+
607
+ **Returns:** Normalized sleep sessions ordered by start ascending.
608
+
609
+ #### `fromFitbitWeight(raw)`
610
+
611
+ Translates a Fitbit weight-log payload into normalized
612
+ {@link WeightEntry} records ordered by `recordedAt` ascending.
613
+
614
+ ```typescript
615
+ function fromFitbitWeight(raw: FitbitWeightResponse): WeightEntry[]
616
+ ```
617
+
618
+ - `raw` — Fitbit's weight response payload.
619
+
620
+ **Returns:** Normalized weight entries.
621
+
622
+ #### `mapSleepStage(level)`
623
+
624
+ Maps Fitbit's per-segment `level` string onto our normalized
625
+ {@link SleepStage} taxonomy. Both `classic` and `stages` payloads are
626
+ supported; unknown values fall through to `unknown`.
627
+
628
+ ```typescript
629
+ function mapSleepStage(level: string): SleepStage
630
+ ```
631
+
632
+ - `level` — The Fitbit sleep level string.
633
+
634
+ **Returns:** A normalized sleep stage.
635
+
636
+ #### `sha256Base64Url(input)`
637
+
638
+ SHA-256-hashes the input string and base64url-encodes the digest. Used
639
+ to derive the PKCE `code_challenge` from the `code_verifier`.
640
+
641
+ ```typescript
642
+ function sha256Base64Url(input: string): Promise<string>
643
+ ```
644
+
645
+ - `input` — The string to hash.
646
+
647
+ **Returns:** A base64url-encoded SHA-256 digest.
648
+
649
+ ### Constants
650
+
651
+ #### `PROVIDER_NAME`
652
+
653
+ Stable provider key used in the credentials store and bond name.
654
+
655
+ ```typescript
656
+ const PROVIDER_NAME: 'fitbit'
657
+ ```
658
+
659
+ #### `wearableFitbitSecretDefinitions`
660
+
661
+ Secret definitions required by the Fitbit wearable bond.
662
+
663
+ ```typescript
664
+ const wearableFitbitSecretDefinitions: SecretDefinition[]
665
+ ```
666
+
667
+ ## Core Interface
668
+
669
+ Implements `@molecule/api-wearable` interface.
670
+
671
+ ## Injection Notes
672
+
673
+ ### Requirements
674
+
675
+ Peer dependencies:
676
+
677
+ - `@molecule/api-bond` ^1.0.1
678
+ - `@molecule/api-http` ^1.0.1
679
+ - `@molecule/api-secrets` ^1.0.1
680
+ - `@molecule/api-wearable` ^1.0.1
681
+
682
+ ### Environment Variables
683
+
684
+ - `OAUTH_FITBIT_CLIENT_ID` _(required)_ — Fitbit OAuth client ID
685
+ - Setup: Register an application at dev.fitbit.com and copy the OAuth 2.0 Client ID.
686
+ - Get it here: [https://dev.fitbit.com/apps](https://dev.fitbit.com/apps)
687
+ - `OAUTH_FITBIT_CLIENT_SECRET` _(optional)_ — Fitbit client secret
688
+ - Setup: Shown on your registered Fitbit application page.
689
+ - Get it here: [https://dev.fitbit.com/apps](https://dev.fitbit.com/apps)
690
+
691
+ ### Runtime Dependencies
692
+
693
+ - `@molecule/api-bond`
694
+ - `@molecule/api-http`
695
+ - `@molecule/api-secrets`
696
+ - `@molecule/api-wearable`
697
+
698
+ **OAuth callback contract for `connect(userId, code)`:** `startAuthorize()`
699
+ stores the PKCE verifier under the returned `state`, but `connect()` looks
700
+ it up by the authorization `code`. Your callback handler must bridge the
701
+ two: validate `state`, `take(state)` the verifier, `put(code, verifier)`
702
+ it back, then call `connect(userId, code)` — or skip the store entirely
703
+ and call `connectWithVerifier(userId, code, verifier)`. Calling `connect()`
704
+ without the re-put fails with 'no PKCE verifier found for supplied code'.
705
+
706
+ Tokens refresh transparently (proactively on expiry, once on 401) and are
707
+ written back to the credentials store before any call returns.
708
+ `disconnect()` always removes the local record even if Fitbit's revoke
709
+ call fails.
710
+
711
+ ## E2E Tests
712
+
713
+ Integration checklist — drive the real UI (live preview), adapt each item to
714
+ this app's actual screens/flows, and check every box off one by one. A box you
715
+ can't check is an integration bug to fix — not a skip. CAVEAT: the device
716
+ vendor's OAuth consent screen and real sensor data can't be driven in the
717
+ sandbox, so verify the connection lifecycle + the data mapping/display you own
718
+ by wiring a stub/test provider (or feeding a sample payload) where the real
719
+ grant/sync would occur — never mock the app's own handlers or UI:
720
+
721
+ - [ ] Connecting a device from the UI runs the provider's `connect(userId, code)`,
722
+ which exchanges the OAuth code and persists a `UserConnection` via the
723
+ `WearableCredentialsStore` (`store.write`), keyed by `(userId, providerName)`.
724
+ Afterward `store.read(userId, name)` returns that connection and the device
725
+ shows "connected"; `disconnect(userId)` removes the record and the UI returns
726
+ to the unlinked state.
727
+ - [ ] Fetching a metric for a real date renders plausible, in-range values in a
728
+ chart/summary: `getDailyActivity().steps` in the thousands, `getDailyHeartRate()`
729
+ `restingHeartRate` ~40-200 bpm, `getDailySleep()` `timeAsleepMinutes` a sane
730
+ number of hours, `getWeight()` `weightKg` a human bodyweight — never
731
+ null/NaN/negative.
732
+ - [ ] Different days/ranges return different data and the dates line up with no
733
+ off-by-one/timezone shift: each rollup's `date` (a `YYYY-MM-DD` `WearableDate`)
734
+ equals the day requested, and every `getWeight(range)` entry's `date` falls
735
+ within `range.start`..`range.end` inclusive.
736
+ - [ ] A day the device wasn't worn/synced shows as a GAP, not a celebrated zero.
737
+ The core zero-defaults `DailyActivity` (a missing day comes back as `steps: 0`,
738
+ `activeMinutes: 0`), so the UI must distinguish "no data" from a real 0 and
739
+ never present an unsynced day as "0 steps achieved".
740
+ - [ ] This core is pull-based — the `WearableProvider` interface has no webhook
741
+ method; data is read on demand via the `getDaily*`/`getWeight` calls. If a
742
+ provider bond wires a sync/subscription callback, delivering a valid callback
743
+ updates the stored data and a forged/unsigned callback is rejected.
744
+ - [ ] PRIVACY/SECURITY — health data is per-user: every `getDaily*`/`getWeight`
745
+ call is scoped by the caller's authenticated `userId` and the store is
746
+ segregated by `(userId, providerName)`, so no id-guessing reaches another
747
+ user's metrics. Device tokens (`accessToken`/`refreshToken`) and provider keys
748
+ stay server-side (the package is server-only) and are never logged in the
749
+ clear — error paths are sanitized so no token or health data leaks into logs.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-wearable-fitbit",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Fitbit Web API bond for molecule.dev — daily activity, sleep, heart-rate, and weight ingestion via OAuth 2.0 PKCE",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -17,7 +17,8 @@
17
17
  }
18
18
  },
19
19
  "files": [
20
- "dist"
20
+ "dist",
21
+ "README.md"
21
22
  ],
22
23
  "keywords": [
23
24
  "molecule",
@@ -30,25 +31,25 @@
30
31
  ],
31
32
  "license": "Apache-2.0",
32
33
  "peerDependencies": {
33
- "@molecule/api-bond": "^1.0.0",
34
- "@molecule/api-http": "^1.0.0",
35
- "@molecule/api-secrets": "^1.0.0",
36
- "@molecule/api-wearable": "^1.0.0"
34
+ "@molecule/api-bond": "^1.0.1",
35
+ "@molecule/api-http": "^1.0.1",
36
+ "@molecule/api-secrets": "^1.0.1",
37
+ "@molecule/api-wearable": "^1.0.1"
37
38
  },
38
39
  "devDependencies": {
39
- "@molecule/api-bond": "1.0.0",
40
- "@molecule/api-http": "1.0.0",
41
- "@molecule/api-wearable": "1.0.0",
40
+ "@molecule/api-bond": "1.0.2",
41
+ "@molecule/api-http": "1.0.2",
42
+ "@molecule/api-wearable": "1.0.2",
42
43
  "@types/node": "26.1.2",
43
44
  "typescript": "6.0.3",
44
- "vitest": "4.1.10"
45
+ "vitest": "4.1.11"
45
46
  },
46
47
  "repository": {
47
48
  "type": "git",
48
49
  "url": "https://github.com/molecule-dev/molecule.git",
49
50
  "directory": "packages/api/bonds/wearable/fitbit"
50
51
  },
51
- "homepage": "https://github.com/molecule-dev/molecule/tree/main/packages/api/bonds/wearable/fitbit",
52
+ "homepage": "https://www.molecule.dev/packages/api-wearable-fitbit",
52
53
  "bugs": "https://github.com/molecule-dev/molecule/issues",
53
54
  "publishConfig": {
54
55
  "access": "public"