Core Interfaces
These interfaces define the contracts that health data providers must implement.
TokenProvider
Manages OAuth tokens for health provider APIs. Implementations handle token storage, refresh, and expiration.
interface TokenProvider {
/**
* Get the current access token.
* May trigger a refresh if the token is expired.
*/
getAccessToken(): Promise<string>;
/**
* Get the refresh token, if available.
*/
getRefreshToken(): Promise<string | undefined>;
/**
* Update stored tokens after a refresh or new authentication.
* @param accessToken - New access token
* @param refreshToken - New refresh token (optional)
* @param expiresAt - Token expiration time (optional)
*/
updateTokens(
accessToken: string,
refreshToken?: string,
expiresAt?: Date
): Promise<void>;
/**
* Check if the current access token is expired.
*/
isExpired(): Promise<boolean>;
}
Implementation Notes
getAccessToken()should automatically refresh expired tokens when possible- Store tokens securely (encrypted at rest, never logged)
expiresAtshould include a buffer (e.g., 5 minutes before actual expiry)
Example Implementation
import { TokenProvider } from '@wellpipe/core';
class DatabaseTokenProvider implements TokenProvider {
constructor(
private userId: string,
private db: Database,
private refreshFn: (refreshToken: string) => Promise<TokenResponse>
) {}
async getAccessToken(): Promise<string> {
const tokens = await this.db.getTokens(this.userId);
if (await this.isExpired()) {
const refreshToken = await this.getRefreshToken();
if (!refreshToken) {
throw new Error('No refresh token available');
}
const newTokens = await this.refreshFn(refreshToken);
await this.updateTokens(
newTokens.accessToken,
newTokens.refreshToken,
new Date(Date.now() + newTokens.expiresIn * 1000)
);
return newTokens.accessToken;
}
return tokens.accessToken;
}
async getRefreshToken(): Promise<string | undefined> {
const tokens = await this.db.getTokens(this.userId);
return tokens?.refreshToken;
}
async updateTokens(
accessToken: string,
refreshToken?: string,
expiresAt?: Date
): Promise<void> {
await this.db.updateTokens(this.userId, {
accessToken,
refreshToken,
expiresAt,
});
}
async isExpired(): Promise<boolean> {
const tokens = await this.db.getTokens(this.userId);
if (!tokens?.expiresAt) return false;
// 5 minute buffer
const bufferMs = 5 * 60 * 1000;
return Date.now() >= tokens.expiresAt.getTime() - bufferMs;
}
}
HealthProvider
Common interface for health data providers. Each provider (WHOOP, Oura, Garmin) implements this interface to normalize their API data.
interface HealthProvider {
/**
* Provider identifier (e.g., 'whoop', 'oura').
*/
readonly name: string;
/**
* Get sleep data for a date range.
*/
getSleep(startDate: string, endDate: string): Promise<SleepData[]>;
/**
* Get recovery data for a date range.
*/
getRecovery(startDate: string, endDate: string): Promise<RecoveryData[]>;
/**
* Get workout data for a date range.
*/
getWorkouts(startDate: string, endDate: string): Promise<WorkoutData[]>;
}
Date Format
All date parameters use ISO 8601 format:
- Date only:
YYYY-MM-DD(e.g.,2024-01-15) - With time:
YYYY-MM-DDTHH:mm:ss.sssZ(e.g.,2024-01-15T08:30:00.000Z)
Return Values
- Methods return empty arrays
[]when no data exists for the date range - Data is sorted by date, oldest first
- Each item includes the original
providerIdfor traceability
Implementation Example
import { HealthProvider, SleepData, RecoveryData, WorkoutData } from '@wellpipe/core';
class WhoopProvider implements HealthProvider {
readonly name = 'whoop';
constructor(private tokenProvider: TokenProvider) {}
async getSleep(startDate: string, endDate: string): Promise<SleepData[]> {
const token = await this.tokenProvider.getAccessToken();
const response = await fetch(
`https://api.prod.whoop.com/developer/v1/activity/sleep?start=${startDate}&end=${endDate}`,
{ headers: { Authorization: `Bearer ${token}` } }
);
const data = await response.json();
return data.records.map(this.normalizeSleep);
}
private normalizeSleep(whoopSleep: WhoopSleepRecord): SleepData {
return {
id: `whoop-${whoopSleep.id}`,
providerId: whoopSleep.id.toString(),
start: whoopSleep.start,
end: whoopSleep.end,
durationMs: whoopSleep.time_in_bed_milliseconds,
isNap: whoopSleep.nap,
score: whoopSleep.score?.sleep_performance_percentage,
stages: {
lightMs: whoopSleep.score?.stage_summary?.total_light_sleep_time_milli ?? 0,
deepMs: whoopSleep.score?.stage_summary?.total_slow_wave_sleep_time_milli ?? 0,
remMs: whoopSleep.score?.stage_summary?.total_rem_sleep_time_milli ?? 0,
awakeMs: whoopSleep.score?.stage_summary?.total_awake_time_milli ?? 0,
},
metrics: {
efficiency: whoopSleep.score?.sleep_efficiency_percentage,
respiratoryRate: whoopSleep.score?.respiratory_rate,
},
};
}
// Similar implementations for getRecovery and getWorkouts...
}
Design Decisions
Why Promises?
All methods return Promises because:
- Token refresh may be required before API calls
- Health APIs are remote and inherently async
- Allows for caching implementations
Why Date Strings?
Date parameters are strings (not Date objects) because:
- JSON serialization is straightforward
- Avoids timezone conversion issues
- Matches how most health APIs accept dates
Why Separate Methods?
We use getSleep(), getRecovery(), getWorkouts() instead of a single getData() because:
- Type safety - each method has a specific return type
- Providers may have different rate limits per endpoint
- Consumers often only need one data type