oura-bun
v0.1.1
Published
Unofficial Oura client for Bun: browser login, web API reads and durable OAuth rotation
Maintainers
Readme
oura-bun
Unofficial Oura client and CLI for Bun. Initial browser authorization, durable single-use refresh-token rotation, paginated API reads, and a separate client for the Oura website's private endpoints.
Not affiliated with Oura. Bring your own Oura account and developer application. No hosted proxy, telemetry, bundled credentials, or stored passwords.
Install with Bun
bun add [email protected]
bunx --no-install oura-bun --helpOr clone and run:
git clone https://github.com/jcpsimmons/oura-bun.git
cd oura-bun
bun install --frozen-lockfile
bun src/cli.tsRequires Bun 1.3 or newer. This is a Bun-native TypeScript package, not a Node.js CLI. GitHub installation also works: bun add github:jcpsimmons/oura-bun.
OAuth: recurring jobs
- Register an application at Oura Developer. Add
http://localhost:8765/callbackto its redirect URIs. - Set
OURA_CLIENT_IDandOURA_CLIENT_SECRETin your environment or a local ignored.env. Never put credentials in command arguments or source control. - Run
bunx --no-install oura-bun login. Complete Oura's login and consent in your browser. Use--no-opento open the printed local URL yourself. - Read records. Refresh happens automatically when needed.
bunx --no-install oura-bun status
bunx --no-install oura-bun get daily_sleep --start 2026-01-01 --end 2026-01-31get writes private data as NDJSON to stdout. Redirect it to a private location, not a public CI log. Login and status never print tokens.
Defaults request daily workout spo2 stress heart_health. For the sleep example, login --scopes daily is sufficient. Choose only the scopes you need; additional scopes require consent again. The defaults do not include personal, email, heartrate, tag or session. --redirect must match your registered loopback callback. --state /absolute/private/file selects the credential file (this option is a filesystem path, not OAuth's anti-CSRF state).
Save this as sleep-count.ts after completing CLI login, then run bun sleep-count.ts. It uses the same credential file as the CLI and prints a record count without exposing sleep data.
import { OAuth, OuraClient, FileStore, type Tokens } from 'oura-bun';
import { join } from 'node:path';
import { homedir } from 'node:os';
const auth = new OAuth({
clientId: process.env.OURA_CLIENT_ID!,
clientSecret: process.env.OURA_CLIENT_SECRET!,
tokenUrl: process.env.OURA_TOKEN_URL,
store: new FileStore<Tokens>(join(homedir(), '.config/oura-bun/oauth.tokens.json')),
});
const client = new OuraClient(auth);
let count = 0;
for await (const _row of client.records('daily_sleep', {
start_date: '2026-01-01',
end_date: '2026-01-31',
})) {
count += 1; // Replace with your own private storage.
}
console.log(`Read ${count} daily sleep records.`);status inspects the local credential file; it does not check whether Oura still accepts the credentials. A successful get confirms API access. See setup, API usage and troubleshooting.
Refresh guarantees and limits
- File stores use exclusive cross-process locks, atomic rename, fsync and mode 0600. The containing directory is created with mode 0700. Existing parent directory permissions are not modified.
- Each client rereads the file under the lock. A stale 401 response cannot consume the replacement token twice.
- A durable
refreshPendingmarker is written before sending the single-use refresh token. A network timeout or crash with an ambiguous outcome requires login again, rather than silently replaying an invalid token. - Rotated credentials must be persisted before access is returned. Upstream error bodies and tokens are not included in errors.
- File locks coordinate processes sharing a local filesystem. They do not coordinate separate machines or independent containers. Implement
Store.exclusiveusing a distributed lock for those environments, and read the latest durable state inside it. Never copy one rotating token into several independent stores. - A crash can leave a lock directory. Confirm the owning process has stopped before removing it. Pending refresh state still requires reauthorization.
- Files are private but not encrypted at rest by this library; use OS disk encryption or a secret-manager
Storefor stronger protection.
Current Oura authorization redirects to moi.ouraring.com/oauth/v2/ext/oauth-authorize. This package defaults token exchanges to the corresponding verified /oauth/v2/ext/oauth-token. Older Oura docs still list api.ouraring.com/oauth/token. Legacy clients can explicitly set OURA_TOKEN_URL to that endpoint. There is no automatic fallback or token replay across issuers.
Website API: experimental
The web adapter was derived from Oura's public JavaScript, including its SSO cookie exchange and daily-data requests. It uses an isolated browser context for sign-in; it does not scrape your normal browser's cookies or ask for your password.
# Use installed Chrome, or install Playwright Chromium with: bunx playwright install chromium
bunx --no-install oura-bun web-login --channel chrome
bunx --no-install oura-bun web-daily --start 2026-01-01 --end 2026-01-31import { OuraWeb } from 'oura-bun/web';
const web = new OuraWeb('/absolute/private/oura.session.json');
await web.login({ channel: 'chrome' }); // user completes Oura login/MFA
const scores = await web.scores('2026-01-01');
const days = await web.daily('2026-01-01', '2026-01-31');Cookies returned by each request are saved before returning data. An expired session produces AuthRequired; rerun web-login to renew through Oura's own SSO. The browser may reuse still-valid SSO cookies from this package's saved session. The website does not expose a browser refresh-token endpoint in its inspected client, so we do not invent one.
Oura says Oura on the Web will be discontinued later in 2026. Use OAuth for durable integrations. Private endpoint contracts may change. The web adapter intentionally exposes only three inspected read endpoints; it does not provide account deletion, consent changes, or organization writes.
See protocol evidence for endpoint provenance, verification and limitations.
Develop
bun install --frozen-lockfile
bun test
bun run typecheckTests use synthetic credentials and temporary private stores. They cover refresh concurrency, durability failures, invalid grants, callback state, pagination, retry limits and web cookie renewal. CI needs no credentials.
