@molecule/api-wearable-oura 1.0.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/LICENSE +115 -0
- package/dist/browser-guard.d.ts +2 -0
- package/dist/browser-guard.d.ts.map +1 -0
- package/dist/browser-guard.js +19 -0
- package/dist/browser-guard.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/provider.d.ts +152 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +556 -0
- package/dist/provider.js.map +1 -0
- package/dist/secrets.d.ts +16 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +30 -0
- package/dist/secrets.js.map +1 -0
- package/dist/types.d.ts +118 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work.
|
|
38
|
+
|
|
39
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
40
|
+
form, that is based on (or derived from) the Work and for which the
|
|
41
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
42
|
+
represent, as a whole, an original work of authorship.
|
|
43
|
+
|
|
44
|
+
"Contribution" shall mean any work of authorship, including the
|
|
45
|
+
original version of the Work and any modifications or additions
|
|
46
|
+
to that Work, that is intentionally submitted to the Licensor for
|
|
47
|
+
inclusion in the Work by the copyright owner or by an individual or
|
|
48
|
+
Legal Entity authorized to submit on behalf of the copyright owner.
|
|
49
|
+
|
|
50
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
51
|
+
on behalf of whom a Contribution has been received by the Licensor and
|
|
52
|
+
subsequently incorporated within the Work.
|
|
53
|
+
|
|
54
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
55
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
56
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
57
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
58
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
59
|
+
Work and such Derivative Works in Source or Object form.
|
|
60
|
+
|
|
61
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
62
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
63
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
64
|
+
patent license to make, have made, use, offer to sell, sell, import,
|
|
65
|
+
and otherwise transfer the Work.
|
|
66
|
+
|
|
67
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
68
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
69
|
+
modifications, and in Source or Object form, provided that You
|
|
70
|
+
meet the following conditions:
|
|
71
|
+
|
|
72
|
+
(a) You must give any other recipients of the Work or
|
|
73
|
+
Derivative Works a copy of this License; and
|
|
74
|
+
|
|
75
|
+
(b) You must cause any modified files to carry prominent notices
|
|
76
|
+
stating that You changed the files; and
|
|
77
|
+
|
|
78
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
79
|
+
that You distribute, all copyright, patent, trademark, and
|
|
80
|
+
attribution notices from the Source form of the Work,
|
|
81
|
+
excluding those notices that do not pertain to any part of
|
|
82
|
+
the Derivative Works; and
|
|
83
|
+
|
|
84
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
85
|
+
distribution, then any Derivative Works that You distribute must
|
|
86
|
+
include a readable copy of the attribution notices contained
|
|
87
|
+
within such NOTICE file.
|
|
88
|
+
|
|
89
|
+
5. Submission of Contributions.
|
|
90
|
+
|
|
91
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
92
|
+
names, trademarks, service marks, or product names of the Licensor.
|
|
93
|
+
|
|
94
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
95
|
+
agreed to in writing, Licensor provides the Work on an "AS IS" BASIS,
|
|
96
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.
|
|
97
|
+
|
|
98
|
+
8. Limitation of Liability. In no event and under no legal theory shall
|
|
99
|
+
any Contributor be liable to You for damages.
|
|
100
|
+
|
|
101
|
+
9. Accepting Warranty or Additional Liability.
|
|
102
|
+
|
|
103
|
+
Copyright 2026 Molecule Dev, Inc.
|
|
104
|
+
|
|
105
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
106
|
+
you may not use this file except in compliance with the License.
|
|
107
|
+
You may obtain a copy of the License at
|
|
108
|
+
|
|
109
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
110
|
+
|
|
111
|
+
Unless required by applicable law or agreed to in writing, software
|
|
112
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
113
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
114
|
+
See the License for the specific language governing permissions and
|
|
115
|
+
limitations under the License.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-guard.d.ts","sourceRoot":"","sources":["../src/browser-guard.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,CAAA"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser guard — `@molecule/api-wearable-oura` is SERVER-ONLY.
|
|
3
|
+
*
|
|
4
|
+
* Generated by scripts/gen-browser-guards.mjs (workspace root) — edit THAT, not this.
|
|
5
|
+
* Evaluating a server package in a browser bundle is always an import-graph mistake
|
|
6
|
+
* (node APIs, secrets); without this guard it surfaces as a cryptic downstream crash
|
|
7
|
+
* ("Buffer is not defined") far from the culprit. Throwing here names the package and
|
|
8
|
+
* the fix at the exact moment the client bundle evaluates it. jsdom tests and SSR are
|
|
9
|
+
* unaffected: the throw requires browser globals AND the absence of a node runtime.
|
|
10
|
+
*/
|
|
11
|
+
const g = globalThis;
|
|
12
|
+
if (g.window !== undefined && g.document !== undefined && !g.process?.versions?.node) {
|
|
13
|
+
throw new Error('@molecule/api-wearable-oura is SERVER-ONLY: it was bundled into browser/client code. Import it only ' +
|
|
14
|
+
'from server code (a server route/function or your API), or dynamic-import it inside ' +
|
|
15
|
+
'the server handler — never from components or shared client modules, and never ' +
|
|
16
|
+
'polyfill Buffer/process to silence this.');
|
|
17
|
+
}
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=browser-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-guard.js","sourceRoot":"","sources":["../src/browser-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,CAAC,GAAG,UAIT,CAAA;AACD,IAAI,CAAC,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,CAAC,QAAQ,KAAK,SAAS,IAAI,CAAC,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACrF,MAAM,IAAI,KAAK,CACb,sGAAsG;QACpG,sFAAsF;QACtF,iFAAiF;QACjF,0CAA0C,CAC7C,CAAA;AACH,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oura Cloud API v2 bond for `@molecule/api-wearable`.
|
|
3
|
+
*
|
|
4
|
+
* Implements daily activity, sleep, and heart-rate ingestion against the
|
|
5
|
+
* Oura Cloud API v2 using OAuth 2.0 with refresh-token rotation. Wires
|
|
6
|
+
* under the `wearable` named-multi-provider category as `'oura'`.
|
|
7
|
+
*
|
|
8
|
+
* Oura does not track body weight, so {@link createProvider} returns
|
|
9
|
+
* `[]` from `getWeight()` — pair Oura with another wearable bond
|
|
10
|
+
* (e.g. `@molecule/api-wearable-fitbit` or `-withings`) when weight
|
|
11
|
+
* data is required.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { setProvider } from '@molecule/api-wearable'
|
|
16
|
+
* import { createProvider, PROVIDER_NAME } from '@molecule/api-wearable-oura'
|
|
17
|
+
*
|
|
18
|
+
* const oura = createProvider({
|
|
19
|
+
* redirectUri: 'https://app.example.com/auth/oura/callback',
|
|
20
|
+
* credentialsStore: myCredentialsStore,
|
|
21
|
+
* })
|
|
22
|
+
*
|
|
23
|
+
* setProvider(PROVIDER_NAME, oura)
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
export * from './browser-guard.js';
|
|
29
|
+
export * from './provider.js';
|
|
30
|
+
export * from './secrets.js';
|
|
31
|
+
export * from './types.js';
|
|
32
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oura Cloud API v2 bond for `@molecule/api-wearable`.
|
|
3
|
+
*
|
|
4
|
+
* Implements daily activity, sleep, and heart-rate ingestion against the
|
|
5
|
+
* Oura Cloud API v2 using OAuth 2.0 with refresh-token rotation. Wires
|
|
6
|
+
* under the `wearable` named-multi-provider category as `'oura'`.
|
|
7
|
+
*
|
|
8
|
+
* Oura does not track body weight, so {@link createProvider} returns
|
|
9
|
+
* `[]` from `getWeight()` — pair Oura with another wearable bond
|
|
10
|
+
* (e.g. `@molecule/api-wearable-fitbit` or `-withings`) when weight
|
|
11
|
+
* data is required.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { setProvider } from '@molecule/api-wearable'
|
|
16
|
+
* import { createProvider, PROVIDER_NAME } from '@molecule/api-wearable-oura'
|
|
17
|
+
*
|
|
18
|
+
* const oura = createProvider({
|
|
19
|
+
* redirectUri: 'https://app.example.com/auth/oura/callback',
|
|
20
|
+
* credentialsStore: myCredentialsStore,
|
|
21
|
+
* })
|
|
22
|
+
*
|
|
23
|
+
* setProvider(PROVIDER_NAME, oura)
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
export * from './browser-guard.js';
|
|
29
|
+
export * from './provider.js';
|
|
30
|
+
export * from './secrets.js';
|
|
31
|
+
export * from './types.js';
|
|
32
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oura Cloud API v2 provider implementation.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the Oura Cloud API v2 behind the
|
|
5
|
+
* {@link import('@molecule/api-wearable').WearableProvider} contract.
|
|
6
|
+
* Each call accepts a host-app `userId`, looks up the persisted
|
|
7
|
+
* {@link import('@molecule/api-wearable').UserConnection} via the bond's
|
|
8
|
+
* {@link import('@molecule/api-wearable').WearableCredentialsStore}, and
|
|
9
|
+
* transparently refreshes the access token on `401` (or proactively when
|
|
10
|
+
* `expiresAt` is in the past). Refreshed tokens are written back to the
|
|
11
|
+
* store before any operation returns.
|
|
12
|
+
*
|
|
13
|
+
* Security invariants (enforced by tests):
|
|
14
|
+
* - Access tokens, refresh tokens, `Authorization` / `Basic` headers,
|
|
15
|
+
* and OAuth `error_description` payloads are NEVER thrown, logged,
|
|
16
|
+
* or surfaced in error messages.
|
|
17
|
+
* - `disconnect` always removes the local record, even if the upstream
|
|
18
|
+
* revoke call fails — leaving a record around is worse than a
|
|
19
|
+
* stranded provider-side token.
|
|
20
|
+
* - `getWeight` returns `[]` — Oura does not track body weight. The
|
|
21
|
+
* bond preserves the `WearableProvider` contract by returning an
|
|
22
|
+
* empty array rather than a non-deterministic error.
|
|
23
|
+
*
|
|
24
|
+
* @module
|
|
25
|
+
*/
|
|
26
|
+
import './secrets.js';
|
|
27
|
+
import type { DailyActivity, HeartRateSummary, SleepSession, SleepStage, SleepStageSegment, WearableDate } from '@molecule/api-wearable';
|
|
28
|
+
import type { OuraProvider, OuraProviderOptions } from './types.js';
|
|
29
|
+
/** Stable provider key used in the credentials store and bond name. */
|
|
30
|
+
export declare const PROVIDER_NAME = "oura";
|
|
31
|
+
interface OuraDailyActivityEntry {
|
|
32
|
+
day: string;
|
|
33
|
+
steps?: number;
|
|
34
|
+
equivalent_walking_distance?: number;
|
|
35
|
+
total_calories?: number;
|
|
36
|
+
active_calories?: number;
|
|
37
|
+
high_activity_time?: number;
|
|
38
|
+
medium_activity_time?: number;
|
|
39
|
+
low_activity_time?: number;
|
|
40
|
+
inactivity_alerts?: number;
|
|
41
|
+
resting_time?: number;
|
|
42
|
+
meters_to_target?: number;
|
|
43
|
+
}
|
|
44
|
+
interface OuraSleepEntry {
|
|
45
|
+
id: string;
|
|
46
|
+
day: string;
|
|
47
|
+
bedtime_start: string;
|
|
48
|
+
bedtime_end: string;
|
|
49
|
+
/** Sleep type — `long_sleep` is the user's main nightly sleep. */
|
|
50
|
+
type?: string;
|
|
51
|
+
total_sleep_duration?: number;
|
|
52
|
+
time_in_bed?: number;
|
|
53
|
+
awake_time?: number;
|
|
54
|
+
light_sleep_duration?: number;
|
|
55
|
+
deep_sleep_duration?: number;
|
|
56
|
+
rem_sleep_duration?: number;
|
|
57
|
+
restless_periods?: number;
|
|
58
|
+
efficiency?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Hypnogram string: each character is one 5-minute segment, with
|
|
61
|
+
* `1=deep`, `2=light`, `3=rem`, `4=awake`.
|
|
62
|
+
*/
|
|
63
|
+
sleep_phase_5_min?: string;
|
|
64
|
+
}
|
|
65
|
+
interface OuraSleepResponse {
|
|
66
|
+
data?: OuraSleepEntry[];
|
|
67
|
+
}
|
|
68
|
+
interface OuraHeartRateSample {
|
|
69
|
+
bpm: number;
|
|
70
|
+
source?: string;
|
|
71
|
+
timestamp: string;
|
|
72
|
+
}
|
|
73
|
+
interface OuraHeartRateResponse {
|
|
74
|
+
data?: OuraHeartRateSample[];
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Base64url-encodes the bytes (RFC 4648 §5 with `=` padding stripped).
|
|
78
|
+
*
|
|
79
|
+
* @param bytes - The bytes to encode.
|
|
80
|
+
* @returns A base64url string.
|
|
81
|
+
*/
|
|
82
|
+
export declare const base64UrlEncode: (bytes: Uint8Array) => string;
|
|
83
|
+
/**
|
|
84
|
+
* Translates an Oura `usercollection/daily_activity` entry into the
|
|
85
|
+
* normalized {@link DailyActivity} shape. Missing fields are
|
|
86
|
+
* zero-defaulted so handlers can sum across days without null guards.
|
|
87
|
+
*
|
|
88
|
+
* @param date - The calendar day requested.
|
|
89
|
+
* @param entry - Oura's daily-activity response entry, or `undefined`.
|
|
90
|
+
* @returns A normalized daily activity record.
|
|
91
|
+
*/
|
|
92
|
+
export declare const fromOuraActivity: (date: WearableDate, entry: OuraDailyActivityEntry | undefined) => DailyActivity;
|
|
93
|
+
/**
|
|
94
|
+
* Maps an Oura `sleep_phase_5_min` digit onto our normalized
|
|
95
|
+
* {@link SleepStage} taxonomy. Oura uses `1=deep`, `2=light`, `3=rem`,
|
|
96
|
+
* `4=awake`; anything else falls through to `unknown`.
|
|
97
|
+
*
|
|
98
|
+
* @param digit - The single hypnogram character.
|
|
99
|
+
* @returns A normalized sleep stage.
|
|
100
|
+
*/
|
|
101
|
+
export declare const mapSleepPhaseDigit: (digit: string) => SleepStage;
|
|
102
|
+
/**
|
|
103
|
+
* Decodes Oura's `sleep_phase_5_min` hypnogram into a list of contiguous
|
|
104
|
+
* {@link SleepStageSegment} records anchored at `bedtimeStart`.
|
|
105
|
+
*
|
|
106
|
+
* Each character represents a 5-minute window. Adjacent same-stage
|
|
107
|
+
* windows are coalesced into a single segment so consumers don't have
|
|
108
|
+
* to do the bookkeeping.
|
|
109
|
+
*
|
|
110
|
+
* @param hypnogram - The Oura hypnogram string.
|
|
111
|
+
* @param bedtimeStart - ISO 8601 timestamp anchoring segment 0.
|
|
112
|
+
* @returns Decoded sleep segments, or `undefined` when no hypnogram exists.
|
|
113
|
+
*/
|
|
114
|
+
export declare const decodeOuraHypnogram: (hypnogram: string | undefined, bedtimeStart: string) => SleepStageSegment[] | undefined;
|
|
115
|
+
/**
|
|
116
|
+
* Translates an Oura `usercollection/sleep` payload into a list of
|
|
117
|
+
* normalized {@link SleepSession} records ordered by start ascending.
|
|
118
|
+
*
|
|
119
|
+
* Oura's `type === 'long_sleep'` corresponds to the user's main nightly
|
|
120
|
+
* sleep — the bond marks any matching session as `isMainSleep: true`,
|
|
121
|
+
* with naps and short sessions as `false`.
|
|
122
|
+
*
|
|
123
|
+
* @param date - Calendar day requested.
|
|
124
|
+
* @param raw - Oura's sleep response payload.
|
|
125
|
+
* @returns Normalized sleep sessions.
|
|
126
|
+
*/
|
|
127
|
+
export declare const fromOuraSleep: (date: WearableDate, raw: OuraSleepResponse) => SleepSession[];
|
|
128
|
+
/**
|
|
129
|
+
* Aggregates an Oura `usercollection/heartrate` payload into the
|
|
130
|
+
* normalized {@link HeartRateSummary} shape.
|
|
131
|
+
*
|
|
132
|
+
* Oura returns per-sample BPM readings rather than a daily resting-HR
|
|
133
|
+
* value or zone breakdown; the bond derives a coarse resting-HR estimate
|
|
134
|
+
* from the minimum sample of the day. Zone data is not reported by this
|
|
135
|
+
* endpoint, so `zones` is left undefined.
|
|
136
|
+
*
|
|
137
|
+
* @param date - Calendar day the response was requested for.
|
|
138
|
+
* @param raw - Oura's heart-rate response payload.
|
|
139
|
+
* @returns A normalized heart-rate summary.
|
|
140
|
+
*/
|
|
141
|
+
export declare const fromOuraHeartRate: (date: WearableDate, raw: OuraHeartRateResponse) => HeartRateSummary;
|
|
142
|
+
/**
|
|
143
|
+
* Creates an Oura wearable provider.
|
|
144
|
+
*
|
|
145
|
+
* @param options - Required: `redirectUri` + `credentialsStore`. Falls
|
|
146
|
+
* back to `OAUTH_OURA_CLIENT_ID` / `OAUTH_OURA_CLIENT_SECRET` env
|
|
147
|
+
* vars when `clientId` / `clientSecret` are omitted.
|
|
148
|
+
* @returns An Oura-flavored {@link OuraProvider}.
|
|
149
|
+
*/
|
|
150
|
+
export declare const createProvider: (options: OuraProviderOptions) => OuraProvider;
|
|
151
|
+
export {};
|
|
152
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAOH,OAAO,cAAc,CAAA;AAKrB,OAAO,KAAK,EACV,aAAa,EACb,gBAAgB,EAChB,YAAY,EACZ,UAAU,EACV,iBAAiB,EAIjB,YAAY,EAGb,MAAM,wBAAwB,CAAA;AAE/B,OAAO,KAAK,EAAsB,YAAY,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAEvF,uEAAuE;AACvE,eAAO,MAAM,aAAa,SAAS,CAAA;AA0BnC,UAAU,sBAAsB;IAC9B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,2BAA2B,CAAC,EAAE,MAAM,CAAA;IACpC,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAMD,UAAU,cAAc;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,MAAM,CAAA;IACX,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,MAAM,CAAA;IACnB,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,UAAU,iBAAiB;IACzB,IAAI,CAAC,EAAE,cAAc,EAAE,CAAA;CACxB;AAED,UAAU,mBAAmB;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,UAAU,qBAAqB;IAC7B,IAAI,CAAC,EAAE,mBAAmB,EAAE,CAAA;CAC7B;AAwCD;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,UAAU,KAAG,MAMnD,CAAA;AAcD;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,GAC3B,MAAM,YAAY,EAClB,OAAO,sBAAsB,GAAG,SAAS,KACxC,aAoBF,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,KAAG,UAalD,CAAA;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB,GAC9B,WAAW,MAAM,GAAG,SAAS,EAC7B,cAAc,MAAM,KACnB,iBAAiB,EAAE,GAAG,SAyBxB,CAAA;AAqBD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,YAAY,EAAE,KAAK,iBAAiB,KAAG,YAAY,EAsBtF,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iBAAiB,GAC5B,MAAM,YAAY,EAClB,KAAK,qBAAqB,KACzB,gBAcF,CAAA;AAMD;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,SAAS,mBAAmB,KAAG,YAqU7D,CAAA"}
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oura Cloud API v2 provider implementation.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the Oura Cloud API v2 behind the
|
|
5
|
+
* {@link import('@molecule/api-wearable').WearableProvider} contract.
|
|
6
|
+
* Each call accepts a host-app `userId`, looks up the persisted
|
|
7
|
+
* {@link import('@molecule/api-wearable').UserConnection} via the bond's
|
|
8
|
+
* {@link import('@molecule/api-wearable').WearableCredentialsStore}, and
|
|
9
|
+
* transparently refreshes the access token on `401` (or proactively when
|
|
10
|
+
* `expiresAt` is in the past). Refreshed tokens are written back to the
|
|
11
|
+
* store before any operation returns.
|
|
12
|
+
*
|
|
13
|
+
* Security invariants (enforced by tests):
|
|
14
|
+
* - Access tokens, refresh tokens, `Authorization` / `Basic` headers,
|
|
15
|
+
* and OAuth `error_description` payloads are NEVER thrown, logged,
|
|
16
|
+
* or surfaced in error messages.
|
|
17
|
+
* - `disconnect` always removes the local record, even if the upstream
|
|
18
|
+
* revoke call fails — leaving a record around is worse than a
|
|
19
|
+
* stranded provider-side token.
|
|
20
|
+
* - `getWeight` returns `[]` — Oura does not track body weight. The
|
|
21
|
+
* bond preserves the `WearableProvider` contract by returning an
|
|
22
|
+
* empty array rather than a non-deterministic error.
|
|
23
|
+
*
|
|
24
|
+
* @module
|
|
25
|
+
*/
|
|
26
|
+
import { randomBytes as nodeRandomBytes } from 'node:crypto';
|
|
27
|
+
// Side-effect import: registers this bond's secret definitions so the
|
|
28
|
+
// runtime registry is populated even when provider.js is imported directly
|
|
29
|
+
// (not through the package barrel).
|
|
30
|
+
import './secrets.js';
|
|
31
|
+
import { getLogger } from '@molecule/api-bond';
|
|
32
|
+
import { get, post } from '@molecule/api-http';
|
|
33
|
+
/** Stable provider key used in the credentials store and bond name. */
|
|
34
|
+
export const PROVIDER_NAME = 'oura';
|
|
35
|
+
const DEFAULT_API_BASE_URL = 'https://api.ouraring.com/v2';
|
|
36
|
+
const DEFAULT_AUTHORIZE_URL = 'https://cloud.ouraring.com/oauth/authorize';
|
|
37
|
+
const DEFAULT_TOKEN_URL = 'https://api.ouraring.com/oauth/token';
|
|
38
|
+
const DEFAULT_REVOKE_URL = 'https://api.ouraring.com/oauth/revoke';
|
|
39
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
40
|
+
const DEFAULT_SCOPES = ['daily', 'heartrate', 'personal', 'session'];
|
|
41
|
+
// Oura's `sleep_phase_5_min` hypnogram uses 5-minute segments per
|
|
42
|
+
// character. RFC link: https://cloud.ouraring.com/v2/docs#operation/sleep_route_v2
|
|
43
|
+
const SLEEP_PHASE_SEGMENT_SECONDS = 300;
|
|
44
|
+
/* -------------------------------------------------------------------------- *
|
|
45
|
+
* Error sanitization.
|
|
46
|
+
* -------------------------------------------------------------------------- */
|
|
47
|
+
/**
|
|
48
|
+
* Sanitizes anything thrown by `@molecule/api-http` (or downstream
|
|
49
|
+
* fetch errors) into a token-free Error. The original error's `message`
|
|
50
|
+
* may contain `Authorization: Bearer …` headers or echoed
|
|
51
|
+
* `error_description` strings that include the access/refresh token.
|
|
52
|
+
*
|
|
53
|
+
* @param scope - Short label of the calling operation (e.g. `"connect"`).
|
|
54
|
+
* @param error - The original error.
|
|
55
|
+
* @returns A new Error safe to throw and log.
|
|
56
|
+
*/
|
|
57
|
+
const sanitizeError = (scope, error) => {
|
|
58
|
+
const httpError = error;
|
|
59
|
+
const status = httpError?.response?.status;
|
|
60
|
+
const code = httpError?.code;
|
|
61
|
+
const parts = [`Oura ${scope} failed`];
|
|
62
|
+
if (status !== undefined)
|
|
63
|
+
parts.push(`status=${status}`);
|
|
64
|
+
if (code)
|
|
65
|
+
parts.push(`code=${code}`);
|
|
66
|
+
return new Error(parts.join(' '));
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Whether an HTTP error indicates an expired/invalid access token.
|
|
70
|
+
*
|
|
71
|
+
* @param error - The original error.
|
|
72
|
+
* @returns True when the response status is 401.
|
|
73
|
+
*/
|
|
74
|
+
const isUnauthorized = (error) => {
|
|
75
|
+
return error?.response?.status === 401;
|
|
76
|
+
};
|
|
77
|
+
/* -------------------------------------------------------------------------- *
|
|
78
|
+
* Encoding helpers.
|
|
79
|
+
* -------------------------------------------------------------------------- */
|
|
80
|
+
/**
|
|
81
|
+
* Base64url-encodes the bytes (RFC 4648 §5 with `=` padding stripped).
|
|
82
|
+
*
|
|
83
|
+
* @param bytes - The bytes to encode.
|
|
84
|
+
* @returns A base64url string.
|
|
85
|
+
*/
|
|
86
|
+
export const base64UrlEncode = (bytes) => {
|
|
87
|
+
return Buffer.from(bytes)
|
|
88
|
+
.toString('base64')
|
|
89
|
+
.replace(/\+/g, '-')
|
|
90
|
+
.replace(/\//g, '_')
|
|
91
|
+
.replace(/=+$/, '');
|
|
92
|
+
};
|
|
93
|
+
/* -------------------------------------------------------------------------- *
|
|
94
|
+
* Normalization helpers.
|
|
95
|
+
* -------------------------------------------------------------------------- */
|
|
96
|
+
const totalActiveMinutes = (entry) => {
|
|
97
|
+
// Oura returns activity-time fields in seconds.
|
|
98
|
+
const high = entry.high_activity_time ?? 0;
|
|
99
|
+
const medium = entry.medium_activity_time ?? 0;
|
|
100
|
+
const low = entry.low_activity_time ?? 0;
|
|
101
|
+
return Math.round((high + medium + low) / 60);
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Translates an Oura `usercollection/daily_activity` entry into the
|
|
105
|
+
* normalized {@link DailyActivity} shape. Missing fields are
|
|
106
|
+
* zero-defaulted so handlers can sum across days without null guards.
|
|
107
|
+
*
|
|
108
|
+
* @param date - The calendar day requested.
|
|
109
|
+
* @param entry - Oura's daily-activity response entry, or `undefined`.
|
|
110
|
+
* @returns A normalized daily activity record.
|
|
111
|
+
*/
|
|
112
|
+
export const fromOuraActivity = (date, entry) => {
|
|
113
|
+
if (!entry) {
|
|
114
|
+
return {
|
|
115
|
+
date,
|
|
116
|
+
steps: 0,
|
|
117
|
+
distanceMeters: 0,
|
|
118
|
+
caloriesOut: 0,
|
|
119
|
+
activeMinutes: 0,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
date: entry.day || date,
|
|
124
|
+
steps: entry.steps ?? 0,
|
|
125
|
+
// `equivalent_walking_distance` is reported in meters.
|
|
126
|
+
distanceMeters: entry.equivalent_walking_distance ?? 0,
|
|
127
|
+
// `total_calories` is the most-comparable Fitbit `caloriesOut` analog
|
|
128
|
+
// (active + BMR equivalent).
|
|
129
|
+
caloriesOut: entry.total_calories ?? 0,
|
|
130
|
+
activeMinutes: totalActiveMinutes(entry),
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Maps an Oura `sleep_phase_5_min` digit onto our normalized
|
|
135
|
+
* {@link SleepStage} taxonomy. Oura uses `1=deep`, `2=light`, `3=rem`,
|
|
136
|
+
* `4=awake`; anything else falls through to `unknown`.
|
|
137
|
+
*
|
|
138
|
+
* @param digit - The single hypnogram character.
|
|
139
|
+
* @returns A normalized sleep stage.
|
|
140
|
+
*/
|
|
141
|
+
export const mapSleepPhaseDigit = (digit) => {
|
|
142
|
+
switch (digit) {
|
|
143
|
+
case '1':
|
|
144
|
+
return 'deep';
|
|
145
|
+
case '2':
|
|
146
|
+
return 'light';
|
|
147
|
+
case '3':
|
|
148
|
+
return 'rem';
|
|
149
|
+
case '4':
|
|
150
|
+
return 'awake';
|
|
151
|
+
default:
|
|
152
|
+
return 'unknown';
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Decodes Oura's `sleep_phase_5_min` hypnogram into a list of contiguous
|
|
157
|
+
* {@link SleepStageSegment} records anchored at `bedtimeStart`.
|
|
158
|
+
*
|
|
159
|
+
* Each character represents a 5-minute window. Adjacent same-stage
|
|
160
|
+
* windows are coalesced into a single segment so consumers don't have
|
|
161
|
+
* to do the bookkeeping.
|
|
162
|
+
*
|
|
163
|
+
* @param hypnogram - The Oura hypnogram string.
|
|
164
|
+
* @param bedtimeStart - ISO 8601 timestamp anchoring segment 0.
|
|
165
|
+
* @returns Decoded sleep segments, or `undefined` when no hypnogram exists.
|
|
166
|
+
*/
|
|
167
|
+
export const decodeOuraHypnogram = (hypnogram, bedtimeStart) => {
|
|
168
|
+
if (!hypnogram || hypnogram.length === 0)
|
|
169
|
+
return undefined;
|
|
170
|
+
const anchor = new Date(bedtimeStart).getTime();
|
|
171
|
+
if (Number.isNaN(anchor))
|
|
172
|
+
return undefined;
|
|
173
|
+
const segments = [];
|
|
174
|
+
let runStage = null;
|
|
175
|
+
let runStartIndex = 0;
|
|
176
|
+
for (let i = 0; i <= hypnogram.length; i += 1) {
|
|
177
|
+
const stage = i < hypnogram.length ? mapSleepPhaseDigit(hypnogram[i]) : null;
|
|
178
|
+
if (stage !== runStage) {
|
|
179
|
+
if (runStage !== null) {
|
|
180
|
+
const startMs = anchor + runStartIndex * SLEEP_PHASE_SEGMENT_SECONDS * 1000;
|
|
181
|
+
const endMs = anchor + i * SLEEP_PHASE_SEGMENT_SECONDS * 1000;
|
|
182
|
+
segments.push({
|
|
183
|
+
stage: runStage,
|
|
184
|
+
start: new Date(startMs).toISOString(),
|
|
185
|
+
end: new Date(endMs).toISOString(),
|
|
186
|
+
durationSeconds: (i - runStartIndex) * SLEEP_PHASE_SEGMENT_SECONDS,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
runStage = stage;
|
|
190
|
+
runStartIndex = i;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return segments;
|
|
194
|
+
};
|
|
195
|
+
const stageSummaryFromOura = (entry) => {
|
|
196
|
+
const out = {};
|
|
197
|
+
// Oura returns stage durations in seconds.
|
|
198
|
+
if (typeof entry.awake_time === 'number') {
|
|
199
|
+
out.awakeMinutes = Math.round(entry.awake_time / 60);
|
|
200
|
+
}
|
|
201
|
+
if (typeof entry.light_sleep_duration === 'number') {
|
|
202
|
+
out.lightMinutes = Math.round(entry.light_sleep_duration / 60);
|
|
203
|
+
}
|
|
204
|
+
if (typeof entry.deep_sleep_duration === 'number') {
|
|
205
|
+
out.deepMinutes = Math.round(entry.deep_sleep_duration / 60);
|
|
206
|
+
}
|
|
207
|
+
if (typeof entry.rem_sleep_duration === 'number') {
|
|
208
|
+
out.remMinutes = Math.round(entry.rem_sleep_duration / 60);
|
|
209
|
+
}
|
|
210
|
+
if (Object.keys(out).length === 0)
|
|
211
|
+
return undefined;
|
|
212
|
+
return out;
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* Translates an Oura `usercollection/sleep` payload into a list of
|
|
216
|
+
* normalized {@link SleepSession} records ordered by start ascending.
|
|
217
|
+
*
|
|
218
|
+
* Oura's `type === 'long_sleep'` corresponds to the user's main nightly
|
|
219
|
+
* sleep — the bond marks any matching session as `isMainSleep: true`,
|
|
220
|
+
* with naps and short sessions as `false`.
|
|
221
|
+
*
|
|
222
|
+
* @param date - Calendar day requested.
|
|
223
|
+
* @param raw - Oura's sleep response payload.
|
|
224
|
+
* @returns Normalized sleep sessions.
|
|
225
|
+
*/
|
|
226
|
+
export const fromOuraSleep = (date, raw) => {
|
|
227
|
+
const entries = raw.data ?? [];
|
|
228
|
+
return entries
|
|
229
|
+
.map((entry) => {
|
|
230
|
+
const totalSleepSeconds = entry.total_sleep_duration ?? 0;
|
|
231
|
+
const timeInBedSeconds = entry.time_in_bed ?? totalSleepSeconds;
|
|
232
|
+
const segments = decodeOuraHypnogram(entry.sleep_phase_5_min, entry.bedtime_start);
|
|
233
|
+
return {
|
|
234
|
+
id: String(entry.id),
|
|
235
|
+
date: entry.day || date,
|
|
236
|
+
start: entry.bedtime_start,
|
|
237
|
+
end: entry.bedtime_end,
|
|
238
|
+
timeInBedMinutes: Math.round(timeInBedSeconds / 60),
|
|
239
|
+
timeAsleepMinutes: Math.round(totalSleepSeconds / 60),
|
|
240
|
+
// Oura returns efficiency as 0-100 already.
|
|
241
|
+
efficiency: entry.efficiency,
|
|
242
|
+
isMainSleep: entry.type === 'long_sleep',
|
|
243
|
+
stageSummary: stageSummaryFromOura(entry),
|
|
244
|
+
segments,
|
|
245
|
+
};
|
|
246
|
+
})
|
|
247
|
+
.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
|
|
248
|
+
};
|
|
249
|
+
/**
|
|
250
|
+
* Aggregates an Oura `usercollection/heartrate` payload into the
|
|
251
|
+
* normalized {@link HeartRateSummary} shape.
|
|
252
|
+
*
|
|
253
|
+
* Oura returns per-sample BPM readings rather than a daily resting-HR
|
|
254
|
+
* value or zone breakdown; the bond derives a coarse resting-HR estimate
|
|
255
|
+
* from the minimum sample of the day. Zone data is not reported by this
|
|
256
|
+
* endpoint, so `zones` is left undefined.
|
|
257
|
+
*
|
|
258
|
+
* @param date - Calendar day the response was requested for.
|
|
259
|
+
* @param raw - Oura's heart-rate response payload.
|
|
260
|
+
* @returns A normalized heart-rate summary.
|
|
261
|
+
*/
|
|
262
|
+
export const fromOuraHeartRate = (date, raw) => {
|
|
263
|
+
const samples = raw.data ?? [];
|
|
264
|
+
if (samples.length === 0) {
|
|
265
|
+
return { date, restingHeartRate: undefined, zones: undefined };
|
|
266
|
+
}
|
|
267
|
+
let min = Number.POSITIVE_INFINITY;
|
|
268
|
+
for (const s of samples) {
|
|
269
|
+
if (typeof s.bpm === 'number' && s.bpm > 0 && s.bpm < min)
|
|
270
|
+
min = s.bpm;
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
date,
|
|
274
|
+
restingHeartRate: Number.isFinite(min) ? min : undefined,
|
|
275
|
+
zones: undefined,
|
|
276
|
+
};
|
|
277
|
+
};
|
|
278
|
+
/* -------------------------------------------------------------------------- *
|
|
279
|
+
* Provider factory.
|
|
280
|
+
* -------------------------------------------------------------------------- */
|
|
281
|
+
/**
|
|
282
|
+
* Creates an Oura wearable provider.
|
|
283
|
+
*
|
|
284
|
+
* @param options - Required: `redirectUri` + `credentialsStore`. Falls
|
|
285
|
+
* back to `OAUTH_OURA_CLIENT_ID` / `OAUTH_OURA_CLIENT_SECRET` env
|
|
286
|
+
* vars when `clientId` / `clientSecret` are omitted.
|
|
287
|
+
* @returns An Oura-flavored {@link OuraProvider}.
|
|
288
|
+
*/
|
|
289
|
+
export const createProvider = (options) => {
|
|
290
|
+
if (!options.redirectUri) {
|
|
291
|
+
throw new Error('Oura bond is missing required option: redirectUri');
|
|
292
|
+
}
|
|
293
|
+
if (!options.credentialsStore) {
|
|
294
|
+
throw new Error('Oura bond is missing required option: credentialsStore');
|
|
295
|
+
}
|
|
296
|
+
const apiBaseUrl = options.apiBaseUrl ?? DEFAULT_API_BASE_URL;
|
|
297
|
+
const authorizeUrl = options.authorizeUrl ?? DEFAULT_AUTHORIZE_URL;
|
|
298
|
+
const tokenUrl = options.tokenUrl ?? DEFAULT_TOKEN_URL;
|
|
299
|
+
const revokeUrl = options.revokeUrl ?? DEFAULT_REVOKE_URL;
|
|
300
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
301
|
+
const scopes = options.scopes ?? DEFAULT_SCOPES;
|
|
302
|
+
const credentialsStore = options.credentialsStore;
|
|
303
|
+
const randomBytes = options.randomBytes ?? ((size) => new Uint8Array(nodeRandomBytes(size)));
|
|
304
|
+
const now = options.now ?? Date.now;
|
|
305
|
+
const logger = getLogger();
|
|
306
|
+
const getClientId = () => {
|
|
307
|
+
const id = options.clientId ?? process.env.OAUTH_OURA_CLIENT_ID;
|
|
308
|
+
if (!id) {
|
|
309
|
+
throw new Error('Oura bond is missing OAUTH_OURA_CLIENT_ID (or clientId option).');
|
|
310
|
+
}
|
|
311
|
+
return id;
|
|
312
|
+
};
|
|
313
|
+
const getClientSecret = () => {
|
|
314
|
+
const secret = options.clientSecret ?? process.env.OAUTH_OURA_CLIENT_SECRET;
|
|
315
|
+
if (!secret) {
|
|
316
|
+
throw new Error('Oura bond is missing OAUTH_OURA_CLIENT_SECRET (or clientSecret option).');
|
|
317
|
+
}
|
|
318
|
+
return secret;
|
|
319
|
+
};
|
|
320
|
+
/**
|
|
321
|
+
* Builds Authorization headers for the OAuth token endpoint. Oura
|
|
322
|
+
* accepts client_secret_basic (Basic auth) for both authorization-code
|
|
323
|
+
* and refresh-token grants.
|
|
324
|
+
*
|
|
325
|
+
* @returns Headers for the token call.
|
|
326
|
+
*/
|
|
327
|
+
const tokenAuthHeaders = () => {
|
|
328
|
+
const clientId = getClientId();
|
|
329
|
+
const clientSecret = getClientSecret();
|
|
330
|
+
const basic = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
|
|
331
|
+
return {
|
|
332
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
333
|
+
accept: 'application/json',
|
|
334
|
+
authorization: `Basic ${basic}`,
|
|
335
|
+
};
|
|
336
|
+
};
|
|
337
|
+
const formEncode = (params) => Object.entries(params)
|
|
338
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
339
|
+
.join('&');
|
|
340
|
+
const callTokenEndpoint = async (params, scope) => {
|
|
341
|
+
const headers = tokenAuthHeaders();
|
|
342
|
+
const body = formEncode(params);
|
|
343
|
+
try {
|
|
344
|
+
const response = await post(tokenUrl, body, {
|
|
345
|
+
headers,
|
|
346
|
+
timeout: timeoutMs,
|
|
347
|
+
});
|
|
348
|
+
const data = response.data;
|
|
349
|
+
if (!data?.access_token || !data?.refresh_token) {
|
|
350
|
+
// Do NOT include any field of `data` in the error — Oura may
|
|
351
|
+
// echo tokens back in `error_description` on some failures.
|
|
352
|
+
throw new Error(`Oura ${scope} failed: malformed token response`);
|
|
353
|
+
}
|
|
354
|
+
return data;
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
if (error instanceof Error && error.message.startsWith(`Oura ${scope} failed:`)) {
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
throw sanitizeError(scope, error);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
const refreshAccessToken = async (connection) => {
|
|
364
|
+
const tokenResponse = await callTokenEndpoint({
|
|
365
|
+
grant_type: 'refresh_token',
|
|
366
|
+
refresh_token: connection.refreshToken,
|
|
367
|
+
}, 'token refresh');
|
|
368
|
+
const refreshed = {
|
|
369
|
+
...connection,
|
|
370
|
+
accessToken: tokenResponse.access_token,
|
|
371
|
+
refreshToken: tokenResponse.refresh_token,
|
|
372
|
+
expiresAt: typeof tokenResponse.expires_in === 'number'
|
|
373
|
+
? now() + tokenResponse.expires_in * 1000
|
|
374
|
+
: undefined,
|
|
375
|
+
scopes: tokenResponse.scope
|
|
376
|
+
? tokenResponse.scope.split(' ').filter(Boolean)
|
|
377
|
+
: connection.scopes,
|
|
378
|
+
};
|
|
379
|
+
await credentialsStore.write(PROVIDER_NAME, refreshed);
|
|
380
|
+
return refreshed;
|
|
381
|
+
};
|
|
382
|
+
const loadConnection = async (userId) => {
|
|
383
|
+
const connection = await credentialsStore.read(userId, PROVIDER_NAME);
|
|
384
|
+
if (!connection) {
|
|
385
|
+
throw new Error(`Oura connection not found for user '${userId}'`);
|
|
386
|
+
}
|
|
387
|
+
return connection;
|
|
388
|
+
};
|
|
389
|
+
/**
|
|
390
|
+
* Calls a function with the user's access token, refreshing the token
|
|
391
|
+
* once on a 401 response (or proactively when `expiresAt` is in the past).
|
|
392
|
+
*
|
|
393
|
+
* @param scope - Calling-operation label for sanitized error messages.
|
|
394
|
+
* @param userId - Host-app user id.
|
|
395
|
+
* @param call - Function performing the HTTP request given an access token.
|
|
396
|
+
* @returns The HTTP response body.
|
|
397
|
+
*/
|
|
398
|
+
const withAuth = async (scope, userId, call) => {
|
|
399
|
+
let connection = await loadConnection(userId);
|
|
400
|
+
let refreshed = false;
|
|
401
|
+
if (typeof connection.expiresAt === 'number' && connection.expiresAt <= now()) {
|
|
402
|
+
connection = await refreshAccessToken(connection);
|
|
403
|
+
refreshed = true;
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
const response = await call(connection.accessToken);
|
|
407
|
+
return response.data;
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
if (!isUnauthorized(error) || refreshed) {
|
|
411
|
+
logger.error(`Oura ${scope} failed`, sanitizeError(scope, error));
|
|
412
|
+
throw sanitizeError(scope, error);
|
|
413
|
+
}
|
|
414
|
+
try {
|
|
415
|
+
connection = await refreshAccessToken(connection);
|
|
416
|
+
const response = await call(connection.accessToken);
|
|
417
|
+
return response.data;
|
|
418
|
+
}
|
|
419
|
+
catch (retryError) {
|
|
420
|
+
if (retryError instanceof Error && retryError.message.startsWith('Oura bond is missing ')) {
|
|
421
|
+
logger.error(`Oura ${scope} failed`, retryError);
|
|
422
|
+
throw retryError;
|
|
423
|
+
}
|
|
424
|
+
logger.error(`Oura ${scope} failed`, sanitizeError(scope, retryError));
|
|
425
|
+
throw sanitizeError(scope, retryError);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
const authedRequestOptions = (accessToken, extra) => ({
|
|
430
|
+
...extra,
|
|
431
|
+
headers: {
|
|
432
|
+
accept: 'application/json',
|
|
433
|
+
...(extra?.headers ?? {}),
|
|
434
|
+
authorization: `Bearer ${accessToken}`,
|
|
435
|
+
},
|
|
436
|
+
timeout: extra?.timeout ?? timeoutMs,
|
|
437
|
+
});
|
|
438
|
+
const buildAuthorizeUrl = (state) => {
|
|
439
|
+
const params = {
|
|
440
|
+
client_id: getClientId(),
|
|
441
|
+
response_type: 'code',
|
|
442
|
+
redirect_uri: options.redirectUri,
|
|
443
|
+
scope: scopes.join(' '),
|
|
444
|
+
state,
|
|
445
|
+
};
|
|
446
|
+
const qs = formEncode(params);
|
|
447
|
+
return authorizeUrl.includes('?') ? `${authorizeUrl}&${qs}` : `${authorizeUrl}?${qs}`;
|
|
448
|
+
};
|
|
449
|
+
const startAuthorize = () => {
|
|
450
|
+
const state = base64UrlEncode(randomBytes(16));
|
|
451
|
+
return { url: buildAuthorizeUrl(state), state };
|
|
452
|
+
};
|
|
453
|
+
/**
|
|
454
|
+
* Performs the authorization-code → token exchange and persists the
|
|
455
|
+
* resulting connection.
|
|
456
|
+
*
|
|
457
|
+
* @param userId - Host-app user id.
|
|
458
|
+
* @param code - OAuth authorization code from the redirect callback.
|
|
459
|
+
* @returns The newly-persisted user connection.
|
|
460
|
+
*/
|
|
461
|
+
const connectInternal = async (userId, code) => {
|
|
462
|
+
const tokenResponse = await callTokenEndpoint({
|
|
463
|
+
grant_type: 'authorization_code',
|
|
464
|
+
code,
|
|
465
|
+
redirect_uri: options.redirectUri,
|
|
466
|
+
}, 'connect');
|
|
467
|
+
const connection = {
|
|
468
|
+
userId,
|
|
469
|
+
// Oura's token endpoint does not return a stable user id; the host
|
|
470
|
+
// can fetch `usercollection/personal_info` separately if it needs
|
|
471
|
+
// one. For now we leave `providerAccountId` empty.
|
|
472
|
+
providerAccountId: '',
|
|
473
|
+
accessToken: tokenResponse.access_token,
|
|
474
|
+
refreshToken: tokenResponse.refresh_token,
|
|
475
|
+
expiresAt: typeof tokenResponse.expires_in === 'number'
|
|
476
|
+
? now() + tokenResponse.expires_in * 1000
|
|
477
|
+
: undefined,
|
|
478
|
+
scopes: tokenResponse.scope ? tokenResponse.scope.split(' ').filter(Boolean) : undefined,
|
|
479
|
+
connectedAt: now(),
|
|
480
|
+
};
|
|
481
|
+
await credentialsStore.write(PROVIDER_NAME, connection);
|
|
482
|
+
return connection;
|
|
483
|
+
};
|
|
484
|
+
return {
|
|
485
|
+
providerName: PROVIDER_NAME,
|
|
486
|
+
startAuthorize,
|
|
487
|
+
async connect(userId, code) {
|
|
488
|
+
return connectInternal(userId, code);
|
|
489
|
+
},
|
|
490
|
+
async refreshConnection(userId) {
|
|
491
|
+
const connection = await loadConnection(userId);
|
|
492
|
+
try {
|
|
493
|
+
return await refreshAccessToken(connection);
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
if (error instanceof Error && error.message.startsWith('Oura ')) {
|
|
497
|
+
throw error;
|
|
498
|
+
}
|
|
499
|
+
throw sanitizeError('refresh', error);
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
async disconnect(userId) {
|
|
503
|
+
let connection = null;
|
|
504
|
+
try {
|
|
505
|
+
connection = await credentialsStore.read(userId, PROVIDER_NAME);
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
logger.error('Oura disconnect read failed', sanitizeError('disconnect', error));
|
|
509
|
+
}
|
|
510
|
+
if (connection) {
|
|
511
|
+
try {
|
|
512
|
+
// Oura's revoke endpoint accepts the access token in the body.
|
|
513
|
+
const headers = tokenAuthHeaders();
|
|
514
|
+
const body = formEncode({ access_token: connection.accessToken });
|
|
515
|
+
await post(revokeUrl, body, { headers, timeout: timeoutMs });
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
// Best-effort revoke — the local record still must be removed
|
|
519
|
+
// below. Sanitize so the access token never leaks.
|
|
520
|
+
logger.error('Oura revoke failed', sanitizeError('disconnect', error));
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
await credentialsStore.remove(userId, PROVIDER_NAME);
|
|
524
|
+
},
|
|
525
|
+
async getDailyActivity(userId, date) {
|
|
526
|
+
const data = await withAuth('getDailyActivity', userId, (accessToken) => get(`${apiBaseUrl}/usercollection/daily_activity?start_date=${encodeURIComponent(date)}&end_date=${encodeURIComponent(date)}`, authedRequestOptions(accessToken)));
|
|
527
|
+
const entry = (data.data ?? []).find((d) => d.day === date) ?? data.data?.[0];
|
|
528
|
+
return fromOuraActivity(date, entry);
|
|
529
|
+
},
|
|
530
|
+
async getDailySleep(userId, date) {
|
|
531
|
+
const data = await withAuth('getDailySleep', userId, (accessToken) => get(`${apiBaseUrl}/usercollection/sleep?start_date=${encodeURIComponent(date)}&end_date=${encodeURIComponent(date)}`, authedRequestOptions(accessToken)));
|
|
532
|
+
return fromOuraSleep(date, data);
|
|
533
|
+
},
|
|
534
|
+
async getDailyHeartRate(userId, date) {
|
|
535
|
+
// Oura's heart-rate endpoint takes ISO-8601 datetimes, not calendar
|
|
536
|
+
// days. We bucket the requested calendar day in UTC; hosts that need
|
|
537
|
+
// local-day buckets can wire their own range.
|
|
538
|
+
const startDatetime = `${date}T00:00:00+00:00`;
|
|
539
|
+
const endDatetime = `${date}T23:59:59+00:00`;
|
|
540
|
+
const data = await withAuth('getDailyHeartRate', userId, (accessToken) => get(`${apiBaseUrl}/usercollection/heartrate?start_datetime=${encodeURIComponent(startDatetime)}&end_datetime=${encodeURIComponent(endDatetime)}`, authedRequestOptions(accessToken)));
|
|
541
|
+
return fromOuraHeartRate(date, data);
|
|
542
|
+
},
|
|
543
|
+
async getWeight(_userId, _range) {
|
|
544
|
+
// Oura does NOT track body weight — there is no equivalent of
|
|
545
|
+
// Fitbit's `body/log/weight` endpoint. Returning an empty array
|
|
546
|
+
// preserves the WearableProvider contract; consumers can detect
|
|
547
|
+
// the absence by checking `result.length === 0` and falling back
|
|
548
|
+
// to a different bond (e.g. `@molecule/api-wearable-fitbit` or
|
|
549
|
+
// `-withings`) for weight readings.
|
|
550
|
+
void _userId;
|
|
551
|
+
void _range;
|
|
552
|
+
return [];
|
|
553
|
+
},
|
|
554
|
+
};
|
|
555
|
+
};
|
|
556
|
+
//# sourceMappingURL=provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oura wearable bond secret definitions — self-registered at import time so the
|
|
3
|
+
* runtime secrets registry (`@molecule/api-secrets`) can drive boot-time
|
|
4
|
+
* configuration reports and actionable "not configured" errors.
|
|
5
|
+
*
|
|
6
|
+
* Content is derived MECHANICALLY from this package's mlcl registry secrets
|
|
7
|
+
* entry (label/instructions/setupUrl/example) via the fleet formula, so
|
|
8
|
+
* packages sharing a key register byte-identical definitions and
|
|
9
|
+
* registration order never matters.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import type { SecretDefinition } from '@molecule/api-secrets';
|
|
14
|
+
/** Secret definitions required by the Oura wearable bond. */
|
|
15
|
+
export declare const wearableOuraSecretDefinitions: SecretDefinition[];
|
|
16
|
+
//# sourceMappingURL=secrets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../src/secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAG7D,6DAA6D;AAC7D,eAAO,MAAM,6BAA6B,EAAE,gBAAgB,EAc3D,CAAA"}
|
package/dist/secrets.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oura wearable bond secret definitions — self-registered at import time so the
|
|
3
|
+
* runtime secrets registry (`@molecule/api-secrets`) can drive boot-time
|
|
4
|
+
* configuration reports and actionable "not configured" errors.
|
|
5
|
+
*
|
|
6
|
+
* Content is derived MECHANICALLY from this package's mlcl registry secrets
|
|
7
|
+
* entry (label/instructions/setupUrl/example) via the fleet formula, so
|
|
8
|
+
* packages sharing a key register byte-identical definitions and
|
|
9
|
+
* registration order never matters.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import { registerSecrets } from '@molecule/api-secrets';
|
|
14
|
+
/** Secret definitions required by the Oura wearable bond. */
|
|
15
|
+
export const wearableOuraSecretDefinitions = [
|
|
16
|
+
{
|
|
17
|
+
key: 'OAUTH_OURA_CLIENT_ID',
|
|
18
|
+
description: 'Oura OAuth client ID — Create an OAuth application in the Oura Cloud developer portal.',
|
|
19
|
+
helpUrl: 'https://cloud.ouraring.com/oauth/applications',
|
|
20
|
+
required: true,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
key: 'OAUTH_OURA_CLIENT_SECRET',
|
|
24
|
+
description: 'Oura client secret — Shown when creating the OAuth application in Oura Cloud.',
|
|
25
|
+
helpUrl: 'https://cloud.ouraring.com/oauth/applications',
|
|
26
|
+
required: true,
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
registerSecrets(wearableOuraSecretDefinitions);
|
|
30
|
+
//# sourceMappingURL=secrets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secrets.js","sourceRoot":"","sources":["../src/secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAEvD,6DAA6D;AAC7D,MAAM,CAAC,MAAM,6BAA6B,GAAuB;IAC/D;QACE,GAAG,EAAE,sBAAsB;QAC3B,WAAW,EACT,wFAAwF;QAC1F,OAAO,EAAE,+CAA+C;QACxD,QAAQ,EAAE,IAAI;KACf;IACD;QACE,GAAG,EAAE,0BAA0B;QAC/B,WAAW,EAAE,+EAA+E;QAC5F,OAAO,EAAE,+CAA+C;QACxD,QAAQ,EAAE,IAAI;KACf;CACF,CAAA;AAED,eAAe,CAAC,6BAA6B,CAAC,CAAA"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oura Ring wearable bond types for molecule.dev.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import type { WearableCredentialsStore, WearableProvider as WearableProviderType } from '@molecule/api-wearable';
|
|
7
|
+
export type { DailyActivity, HeartRateSummary, HeartRateZone, SleepSession, SleepStage, SleepStageSegment, SleepStageSummary, UserConnection, WearableCredentialsStore, WearableDate, WearableDateRange, WearableProvider, WeightEntry, } from '@molecule/api-wearable';
|
|
8
|
+
declare global {
|
|
9
|
+
namespace NodeJS {
|
|
10
|
+
/**
|
|
11
|
+
* Process Env interface.
|
|
12
|
+
*/
|
|
13
|
+
interface ProcessEnv {
|
|
14
|
+
/**
|
|
15
|
+
* The Oura OAuth client ID. Required to mint and refresh access
|
|
16
|
+
* tokens.
|
|
17
|
+
*
|
|
18
|
+
* @see https://cloud.ouraring.com/docs/authentication
|
|
19
|
+
*/
|
|
20
|
+
OAUTH_OURA_CLIENT_ID?: string;
|
|
21
|
+
/**
|
|
22
|
+
* The Oura OAuth client secret. Required for the
|
|
23
|
+
* `authorization_code` and `refresh_token` grant flows.
|
|
24
|
+
*
|
|
25
|
+
* @see https://cloud.ouraring.com/docs/authentication
|
|
26
|
+
*/
|
|
27
|
+
OAUTH_OURA_CLIENT_SECRET?: string;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Configuration options for {@link createProvider}.
|
|
33
|
+
*/
|
|
34
|
+
export interface OuraProviderOptions {
|
|
35
|
+
/**
|
|
36
|
+
* OAuth client id. Defaults to `process.env.OAUTH_OURA_CLIENT_ID`.
|
|
37
|
+
*/
|
|
38
|
+
clientId?: string;
|
|
39
|
+
/**
|
|
40
|
+
* OAuth client secret. Defaults to
|
|
41
|
+
* `process.env.OAUTH_OURA_CLIENT_SECRET`.
|
|
42
|
+
*/
|
|
43
|
+
clientSecret?: string;
|
|
44
|
+
/**
|
|
45
|
+
* OAuth redirect URI registered with the Oura application.
|
|
46
|
+
*/
|
|
47
|
+
redirectUri: string;
|
|
48
|
+
/**
|
|
49
|
+
* Persists user connections (access + refresh tokens). Required.
|
|
50
|
+
*/
|
|
51
|
+
credentialsStore: WearableCredentialsStore;
|
|
52
|
+
/**
|
|
53
|
+
* OAuth scopes to request when starting authorization. Defaults to the
|
|
54
|
+
* union needed for daily activity, sleep, heart-rate, and personal
|
|
55
|
+
* info reads.
|
|
56
|
+
*/
|
|
57
|
+
scopes?: readonly string[];
|
|
58
|
+
/**
|
|
59
|
+
* Override the Oura Cloud API base URL. Defaults to
|
|
60
|
+
* `https://api.ouraring.com/v2`.
|
|
61
|
+
*/
|
|
62
|
+
apiBaseUrl?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Override the Oura OAuth authorize endpoint. Defaults to
|
|
65
|
+
* `https://cloud.ouraring.com/oauth/authorize`.
|
|
66
|
+
*/
|
|
67
|
+
authorizeUrl?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Override the Oura OAuth token endpoint. Defaults to
|
|
70
|
+
* `https://api.ouraring.com/oauth/token`.
|
|
71
|
+
*/
|
|
72
|
+
tokenUrl?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Override the Oura OAuth token-revoke endpoint. Defaults to
|
|
75
|
+
* `https://api.ouraring.com/oauth/revoke`.
|
|
76
|
+
*/
|
|
77
|
+
revokeUrl?: string;
|
|
78
|
+
/**
|
|
79
|
+
* Request timeout in milliseconds. Defaults to `15_000`.
|
|
80
|
+
*/
|
|
81
|
+
timeoutMs?: number;
|
|
82
|
+
/**
|
|
83
|
+
* Optional override for the random-bytes generator used for the OAuth
|
|
84
|
+
* `state` parameter. Tests inject a deterministic generator.
|
|
85
|
+
*/
|
|
86
|
+
randomBytes?: (size: number) => Uint8Array;
|
|
87
|
+
/**
|
|
88
|
+
* Optional clock override, primarily for tests. Defaults to `Date.now`.
|
|
89
|
+
*/
|
|
90
|
+
now?: () => number;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Result of {@link OuraProvider.startAuthorize} — the URL the host
|
|
94
|
+
* should redirect the user to plus the `state` value to round-trip.
|
|
95
|
+
*/
|
|
96
|
+
export interface OuraAuthorizeStart {
|
|
97
|
+
/** Oura OAuth authorize URL. */
|
|
98
|
+
url: string;
|
|
99
|
+
/** Opaque `state` value the host MUST verify on the callback. */
|
|
100
|
+
state: string;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Public surface returned by {@link createProvider}. Extends the
|
|
104
|
+
* stack-neutral {@link import('@molecule/api-wearable').WearableProvider}
|
|
105
|
+
* with an Oura-flavored `startAuthorize()` helper for hosts that want
|
|
106
|
+
* the bond to build the authorize URL.
|
|
107
|
+
*/
|
|
108
|
+
export interface OuraProvider extends WearableProviderType {
|
|
109
|
+
/**
|
|
110
|
+
* Builds the Oura authorize URL with a random `state` value. The host
|
|
111
|
+
* is responsible for round-tripping `state` through the OAuth callback
|
|
112
|
+
* and verifying it before calling {@link OuraProvider.connect}.
|
|
113
|
+
*
|
|
114
|
+
* @returns The URL to redirect to and the `state` value.
|
|
115
|
+
*/
|
|
116
|
+
startAuthorize(): OuraAuthorizeStart;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,wBAAwB,EACxB,gBAAgB,IAAI,oBAAoB,EACzC,MAAM,wBAAwB,CAAA;AAE/B,YAAY,EACV,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,UAAU,EACV,iBAAiB,EACjB,iBAAiB,EACjB,cAAc,EACd,wBAAwB,EACxB,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,WAAW,GACZ,MAAM,wBAAwB,CAAA;AAE/B,OAAO,CAAC,MAAM,CAAC;IAEb,UAAU,MAAM,CAAC;QACf;;WAEG;QACH,UAAiB,UAAU;YACzB;;;;;eAKG;YACH,oBAAoB,CAAC,EAAE,MAAM,CAAA;YAE7B;;;;;eAKG;YACH,wBAAwB,CAAC,EAAE,MAAM,CAAA;SAClC;KACF;CACF;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;OAEG;IACH,WAAW,EAAE,MAAM,CAAA;IACnB;;OAEG;IACH,gBAAgB,EAAE,wBAAwB,CAAA;IAC1C;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC1B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,UAAU,CAAA;IAC1C;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,gCAAgC;IAChC,GAAG,EAAE,MAAM,CAAA;IACX,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;;;;GAKG;AACH,MAAM,WAAW,YAAa,SAAQ,oBAAoB;IACxD;;;;;;OAMG;IACH,cAAc,IAAI,kBAAkB,CAAA;CACrC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@molecule/api-wearable-oura",
|
|
3
|
+
"version": "1.0.0",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"test": "vitest run",
|
|
11
|
+
"test:watch": "vitest"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"molecule",
|
|
24
|
+
"wearable",
|
|
25
|
+
"oura",
|
|
26
|
+
"ring",
|
|
27
|
+
"sleep",
|
|
28
|
+
"readiness",
|
|
29
|
+
"heart-rate",
|
|
30
|
+
"oauth"
|
|
31
|
+
],
|
|
32
|
+
"license": "Apache-2.0",
|
|
33
|
+
"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"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@molecule/api-bond": "1.0.0",
|
|
41
|
+
"@molecule/api-http": "1.0.0",
|
|
42
|
+
"@molecule/api-wearable": "1.0.0",
|
|
43
|
+
"@types/node": "26.1.2",
|
|
44
|
+
"typescript": "6.0.3",
|
|
45
|
+
"vitest": "4.1.10"
|
|
46
|
+
},
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "https://github.com/molecule-dev/molecule.git",
|
|
50
|
+
"directory": "packages/api/bonds/wearable/oura"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/molecule-dev/molecule/tree/main/packages/api/bonds/wearable/oura",
|
|
53
|
+
"bugs": "https://github.com/molecule-dev/molecule/issues",
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
}
|
|
57
|
+
}
|