jcpsimmons/oura-bun

Unofficial Oura client for Bun with browser auth, durable OAuth refresh, and inspected web API reads

0

stars

4

commits

TypeScript

primary language

Sep 5, 2026

updated

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 oura-bun@0.1.1
bunx --no-install oura-bun --help

Or clone and run:

git clone https://github.com/jcpsimmons/oura-bun.git
cd oura-bun
bun install --frozen-lockfile
bun src/cli.ts

Requires 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

  1. Register an application at Oura Developer. Add http://localhost:8765/callback to its redirect URIs.
  2. Set OURA_CLIENT_ID and OURA_CLIENT_SECRET in your environment or a local ignored .env. Never put credentials in command arguments or source control.
  3. Run bunx --no-install oura-bun login. Complete Oura's login and consent in your browser. Use --no-open to open the printed local URL yourself.
  4. 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-31

get 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 refreshPending marker 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.exclusive using 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 Store for 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-31
import { 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 typecheck

Tests 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.

Contributors

jcpsimmons

4 commits

jcpsimmons/oura-bun

Unofficial Oura client for Bun with browser auth, durable OAuth refresh, and inspected web API reads

0

stars

4

commits

TypeScript

primary language

Sep 5, 2026

updated

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 oura-bun@0.1.1
bunx --no-install oura-bun --help

Or clone and run:

git clone https://github.com/jcpsimmons/oura-bun.git
cd oura-bun
bun install --frozen-lockfile
bun src/cli.ts

Requires 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

  1. Register an application at Oura Developer. Add http://localhost:8765/callback to its redirect URIs.
  2. Set OURA_CLIENT_ID and OURA_CLIENT_SECRET in your environment or a local ignored .env. Never put credentials in command arguments or source control.
  3. Run bunx --no-install oura-bun login. Complete Oura's login and consent in your browser. Use --no-open to open the printed local URL yourself.
  4. 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-31

get 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 refreshPending marker 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.exclusive using 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 Store for 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-31
import { 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 typecheck

Tests 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.

Contributors

jcpsimmons

4 commits

Languages

TypeScript

100.0%