@molecule/api-wearable
v1.0.2
Published
Wearable cloud-API ingestion core for molecule.dev — normalized contract for Fitbit, Oura, Withings, and other consumer wearable providers
Maintainers
Readme
@molecule/api-wearable
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Wearable core interface for molecule.dev (server-side).
Defines a stack-neutral, provider-neutral contract for wearable
cloud-API bonds (Fitbit, Oura, Withings, Garmin, etc.) consumed by
handlers and background sync jobs. Wearable bonds register as a
named multi-provider — multiple wearable providers may be active
for a single user — so wiring uses the named bond API:
bond('wearable', 'fitbit', provider).
Quick Start
import { setProvider, getProvider } from '@molecule/api-wearable'
import { createProvider as createFitbit } from '@molecule/api-wearable-fitbit'
setProvider('fitbit', createFitbit({ credentialsStore, redirectUri: '...' }))
const fitbit = getProvider('fitbit')
const today = await fitbit.getDailyActivity('user-1', '2026-05-01')Type
core
Installation
npm install @molecule/api-wearable @molecule/api-bond @molecule/api-i18nAPI
Interfaces
DailyActivity
Daily activity rollup. All fields are zero-defaulted by the bond when the provider does not return a value, so handlers can sum across days without null-checks.
interface DailyActivity {
/** Calendar day this rollup covers (`YYYY-MM-DD`). */
date: WearableDate
/** Total step count for the day. */
steps: number
/** Total distance traveled, in meters. */
distanceMeters: number
/** Total calories burned (active + BMR), in kilocalories. */
caloriesOut: number
/** Active minutes (provider-specific intensity definitions). */
activeMinutes: number
/** Floors climbed, when reported. */
floors?: number
/** Total elevation gain, in meters, when reported. */
elevationMeters?: number
/** Resting heart-rate, in beats per minute, when reported. */
restingHeartRate?: number
}HeartRateSummary
Daily heart-rate summary.
interface HeartRateSummary {
/** Calendar day this summary covers (`YYYY-MM-DD`). */
date: WearableDate
/** Resting heart-rate, in beats per minute, when reported. */
restingHeartRate?: number
/** Per-zone breakdown, when reported. */
zones?: HeartRateZone[]
}HeartRateZone
Heart-rate zone definition (rest/fat-burn/cardio/peak or provider-named).
interface HeartRateZone {
/** Zone label as reported by the provider. */
name: string
/** Lower bound of the zone, in beats per minute (inclusive). */
minBpm: number
/** Upper bound of the zone, in beats per minute (exclusive). */
maxBpm: number
/** Minutes spent in the zone for this period. */
minutes: number
/** Calories burned in the zone, in kilocalories. */
caloriesOut?: number
}SleepSession
One sleep session. Most users have exactly one per night, but providers report naps as separate sessions, so handlers must sum across the array.
interface SleepSession {
/** Provider-specific session id. */
id: string
/** Calendar day this session is bucketed under (`YYYY-MM-DD`). */
date: WearableDate
/** ISO 8601 sleep start. */
start: string
/** ISO 8601 sleep end. */
end: string
/** Total time in bed, in minutes. */
timeInBedMinutes: number
/** Total time asleep, in minutes (excludes `awake` segments). */
timeAsleepMinutes: number
/** Sleep efficiency percentage (0-100). */
efficiency?: number
/** Whether this session is the user's primary sleep for the day. */
isMainSleep: boolean
/** Per-stage minute totals when the provider reports stages. */
stageSummary?: SleepStageSummary
/** Per-segment stage breakdown when the provider reports stages. */
segments?: SleepStageSegment[]
}SleepStageSegment
A contiguous block of a single sleep stage within a sleep session.
interface SleepStageSegment {
/** Normalized stage. */
stage: SleepStage
/** ISO 8601 segment start. */
start: string
/** ISO 8601 segment end. */
end: string
/** Segment duration in seconds (provider-reported when available). */
durationSeconds: number
}SleepStageSummary
Per-stage totals for a sleep session, in minutes. Optional — providers
that only report the coarse "asleep" classification will omit
light/deep/rem.
interface SleepStageSummary {
/** Minutes spent awake during the session. */
awakeMinutes?: number
/** Minutes in light sleep. */
lightMinutes?: number
/** Minutes in deep sleep. */
deepMinutes?: number
/** Minutes in REM sleep. */
remMinutes?: number
/** Minutes restless (legacy classifications). */
restlessMinutes?: number
}UserConnection
Per-user OAuth tokens minted by {@link WearableProvider.connect} and rotated by {@link WearableProvider.refreshConnection}. Bonds persist these via a caller-supplied {@link WearableCredentialsStore}.
Token strings are NEVER thrown, logged, or echoed back in error messages — implementations must sanitize all error paths.
interface UserConnection {
/** The user owning the connection in the host application. */
userId: string
/** Provider-specific account identifier (e.g. Fitbit `user_id`). */
providerAccountId: string
/** Current access token. */
accessToken: string
/** Refresh token used to mint new access tokens. */
refreshToken: string
/** Optional epoch-millis timestamp at which the access token expires. */
expiresAt?: number
/** Granted OAuth scopes (provider-specific names). */
scopes?: string[]
/** Epoch-millis timestamp at which the connection was first established. */
connectedAt: number
}WearableCredentialsStore
Persistence contract for {@link UserConnection} records. Implementations are responsible for at-rest encryption (refresh tokens are bearer credentials and MUST be stored securely).
The same store can back any number of provider bonds (Fitbit, Oura,
Withings, etc.) — segregation is by (userId, providerName) pair.
interface WearableCredentialsStore {
/**
* Looks up the connection for `(userId, providerName)`.
*
* @param userId - Host-app user identifier.
* @param providerName - Provider key, e.g. `"fitbit"`.
* @returns The stored connection or `null` if none.
*/
read(userId: string, providerName: string): Promise<UserConnection | null>
/**
* Persists a new or refreshed connection.
*
* @param providerName - Provider key, e.g. `"fitbit"`.
* @param connection - The connection record to write.
*/
write(providerName: string, connection: UserConnection): Promise<void>
/**
* Deletes the connection for `(userId, providerName)`.
*
* @param userId - Host-app user identifier.
* @param providerName - Provider key.
*/
remove(userId: string, providerName: string): Promise<void>
}WearableDateRange
Inclusive date range.
interface WearableDateRange {
/** Lower-bound calendar day (`YYYY-MM-DD`), inclusive. */
start: WearableDate
/** Upper-bound calendar day (`YYYY-MM-DD`), inclusive. */
end: WearableDate
}WearableProvider
Wearable provider contract.
Each method takes a host-app userId rather than per-call credentials.
Implementations look up persisted {@link UserConnection} records via the
caller-supplied {@link WearableCredentialsStore}, transparently refresh
expired access tokens (writing the rotated record back to the store),
and surface refreshes as a side-effect of normal data calls.
Token-bearing values (access tokens, refresh tokens, Authorization
headers, OAuth error_description payloads that may include the token)
MUST NEVER be thrown, logged, or returned in error messages — every
error path must be sanitized.
interface WearableProvider {
/** Stable provider key used for credential-store segregation, e.g. `"fitbit"`. */
readonly providerName: string
/**
* Reads the daily activity rollup for `(userId, date)`.
*
* @param userId - Host-app user identifier.
* @param date - Calendar day (`YYYY-MM-DD`).
* @returns Normalized daily activity.
*/
getDailyActivity(userId: string, date: WearableDate): Promise<DailyActivity>
/**
* Reads sleep sessions bucketed under `(userId, date)`. Most days return
* a single primary session; multi-nap days return multiple.
*
* @param userId - Host-app user identifier.
* @param date - Calendar day (`YYYY-MM-DD`).
* @returns All sleep sessions bucketed under that day.
*/
getDailySleep(userId: string, date: WearableDate): Promise<SleepSession[]>
/**
* Reads the daily heart-rate summary for `(userId, date)`.
*
* @param userId - Host-app user identifier.
* @param date - Calendar day (`YYYY-MM-DD`).
* @returns Normalized daily heart-rate summary.
*/
getDailyHeartRate(userId: string, date: WearableDate): Promise<HeartRateSummary>
/**
* Reads body-weight entries for the user across `range`. Providers cap
* how far back a single call may reach — implementations should clamp
* `range` to the provider's documented maximum and return only the
* available entries.
*
* @param userId - Host-app user identifier.
* @param range - Inclusive calendar-day range.
* @returns Weight entries, ordered by `recordedAt` ascending.
*/
getWeight(userId: string, range: WearableDateRange): Promise<WeightEntry[]>
/**
* Exchanges an OAuth authorization `code` for tokens and persists the
* resulting {@link UserConnection} via the bond's credentials store.
*
* @param userId - Host-app user identifier (the local user accepting the link).
* @param code - Authorization code from the OAuth redirect callback.
* @returns The freshly-minted connection (already persisted).
*/
connect(userId: string, code: string): Promise<UserConnection>
/**
* Forces a refresh of the user's access token using the stored refresh
* token. The rotated connection is persisted before being returned.
*
* @param userId - Host-app user identifier.
* @returns The rotated connection (already persisted).
*/
refreshConnection(userId: string): Promise<UserConnection>
/**
* Revokes (best-effort) and removes the user's connection record.
*
* Implementations SHOULD attempt to revoke the token at the provider but
* MUST always remove the local record even if revocation fails — leaking
* a record after `disconnect` is worse than a stranded provider-side
* token.
*
* @param userId - Host-app user identifier.
*/
disconnect(userId: string): Promise<void>
}WeightEntry
One body-weight reading.
interface WeightEntry {
/** ISO 8601 timestamp at which the reading was taken. */
recordedAt: string
/** Calendar day of the reading (`YYYY-MM-DD`). */
date: WearableDate
/** Weight in kilograms. */
weightKg: number
/** Body-fat percentage (0-100), when reported. */
bodyFatPercent?: number
/** BMI (kg / m²), when reported. */
bmi?: number
/** Provider-specific entry id. */
id?: string
}Types
SleepStage
Sleep stage taxonomy normalized across providers.
awake— periods awake during a sleep sessionlight,deep,rem— modern stage classificationsrestless,asleep— legacy/coarse classifications used by some providersunknown— fallback for unmapped values
type SleepStage = 'awake' | 'light' | 'deep' | 'rem' | 'restless' | 'asleep' | 'unknown'WearableDate
Calendar-day identifier in YYYY-MM-DD format. Wearable providers
universally bucket activity/sleep/HR data by local-day, so the core
exchanges date strings (not absolute timestamps) for per-day reads.
type WearableDate = stringFunctions
getOptionalProvider(name)
Retrieves the bonded wearable provider for name, or null when none
is bonded. Use this when a missing connection is a normal state (e.g.
the user hasn't linked a Fitbit account yet).
function getOptionalProvider(name: string): WearableProvider | nullname— Provider key.
Returns: The bonded provider, or null.
getProvider(name)
Retrieves the bonded wearable provider for name, throwing if none is
configured.
function getProvider(name: string): WearableProvidername— Provider key, e.g.'fitbit'.
Returns: The bonded wearable provider.
hasProvider(name)
Checks whether a wearable provider is bonded under name.
function hasProvider(name: string): booleanname— Provider key.
Returns: true if bonded.
listProviders()
Lists every currently-bonded wearable provider name (e.g.
['fitbit', 'oura']). Useful for rendering a "linked accounts"
settings page or fanning a sync job out across all bonded providers.
function listProviders(): string[]Returns: The list of bonded provider keys.
setProvider(name, provider)
Registers a wearable provider under the given name (e.g. 'fitbit').
Bond packages call this during application startup.
function setProvider(name: string, provider: WearableProvider): voidname— Stable provider key, matching {@link WearableProvider.providerName}.provider— The wearable provider implementation to bond.
Available Providers
| Provider | Package |
| --------- | ------------------------------- |
| Fitbit | @molecule/api-wearable-fitbit |
| Oura Ring | @molecule/api-wearable-oura |
Injection Notes
Requirements
Peer dependencies:
@molecule/api-bond^1.0.1@molecule/api-i18n^1.0.1
Runtime Dependencies
@molecule/api-bond@molecule/api-i18n
E2E Tests
Integration checklist — drive the real UI (live preview), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip. CAVEAT: the device vendor's OAuth consent screen and real sensor data can't be driven in the sandbox, so verify the connection lifecycle + the data mapping/display you own by wiring a stub/test provider (or feeding a sample payload) where the real grant/sync would occur — never mock the app's own handlers or UI:
- [ ] Connecting a device from the UI runs the provider's
connect(userId, code), which exchanges the OAuth code and persists aUserConnectionvia theWearableCredentialsStore(store.write), keyed by(userId, providerName). Afterwardstore.read(userId, name)returns that connection and the device shows "connected";disconnect(userId)removes the record and the UI returns to the unlinked state. - [ ] Fetching a metric for a real date renders plausible, in-range values in a
chart/summary:
getDailyActivity().stepsin the thousands,getDailyHeartRate()restingHeartRate~40-200 bpm,getDailySleep()timeAsleepMinutesa sane number of hours,getWeight()weightKga human bodyweight — never null/NaN/negative. - [ ] Different days/ranges return different data and the dates line up with no
off-by-one/timezone shift: each rollup's
date(aYYYY-MM-DDWearableDate) equals the day requested, and everygetWeight(range)entry'sdatefalls withinrange.start..range.endinclusive. - [ ] A day the device wasn't worn/synced shows as a GAP, not a celebrated zero.
The core zero-defaults
DailyActivity(a missing day comes back assteps: 0,activeMinutes: 0), so the UI must distinguish "no data" from a real 0 and never present an unsynced day as "0 steps achieved". - [ ] This core is pull-based — the
WearableProviderinterface has no webhook method; data is read on demand via thegetDaily*/getWeightcalls. If a provider bond wires a sync/subscription callback, delivering a valid callback updates the stored data and a forged/unsigned callback is rejected. - [ ] PRIVACY/SECURITY — health data is per-user: every
getDaily*/getWeightcall is scoped by the caller's authenticateduserIdand the store is segregated by(userId, providerName), so no id-guessing reaches another user's metrics. Device tokens (accessToken/refreshToken) and provider keys stay server-side (the package is server-only) and are never logged in the clear — error paths are sanitized so no token or health data leaks into logs.
