Skip to main content

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)
  • expiresAt should 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 providerId for 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:

  1. Token refresh may be required before API calls
  2. Health APIs are remote and inherently async
  3. Allows for caching implementations

Why Date Strings?

Date parameters are strings (not Date objects) because:

  1. JSON serialization is straightforward
  2. Avoids timezone conversion issues
  3. Matches how most health APIs accept dates

Why Separate Methods?

We use getSleep(), getRecovery(), getWorkouts() instead of a single getData() because:

  1. Type safety - each method has a specific return type
  2. Providers may have different rate limits per endpoint
  3. Consumers often only need one data type