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