Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 22 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# irdata_js

JavaScript library to interact with the iRacing /data API.
A JavaScript library to interact with the iRacing /data API.

## Installation

Expand All @@ -13,6 +13,15 @@ npm install irdata_js
- **Node.js**: v20.0.0 or newer.
- **Browsers**: Modern browsers supporting ES2022 (Chrome 100+, Firefox 100+, Safari 15.4+).

## Client Registration

Before using the library, you must register your application with iRacing to obtain a Client ID and configure your Redirect URI.

Please refer to the [official iRacing Client Registration documentation](https://oauth.iracing.com/oauth2/book/client_registration.html).

> [!NOTE]
> It may take up to **10 business days** for registration requests to be processed.

## CDN Usage

For direct usage in the browser without a build step, you can load the library via a CDN. The library is exposed as the global `irdata` variable.
Expand Down Expand Up @@ -84,7 +93,7 @@ Since the iRacing API and S3 buckets do not support CORS, you need to use a prox

#### Web / Browser (OAuth 2.0 PKCE)

To authenticate in the browser, you need to generate an authorization URL, redirect the user, and then handle the callback.
To authenticate in the browser, you need to generate an authorization URL, redirect the user, and then handle the return.

**Step 1: Generate Auth URL and Redirect**

Expand All @@ -93,18 +102,17 @@ const url = await client.auth.generateAuthUrl();
window.location.href = url;
```

**Step 2: Handle Callback**
**Step 2: Handle Return & Restore Session**

On your redirect page, capture the `code` from the URL:
Simply call `handleAuthentication()` on every page that uses the library. This single method handles:
- Exchanging the authorization code (when returning from the iRacing login page).
- Refreshing the access token (if a refresh token is stored).
- Verifying an existing session.

```javascript
const params = new URLSearchParams(window.location.search);
const code = params.get('code');

if (code) {
await client.auth.handleCallback(code);
// Success! The client is now authenticated with an access token.
}
// This should run on every page load of your application,
// including the redirectUri page.
const isAuthenticated = await client.auth.handleAuthentication();
```

### 3. Fetch Data
Expand Down Expand Up @@ -182,7 +190,9 @@ npm run build

This repository includes a local development proxy server and a demo application to test the OAuth flow and API interaction, avoiding CORS issues during development.

1. Create a file named `config.json` in the `demo/` directory (ignored by git) with your configuration:
1. Create a file named `config.json` in the `demo/` directory (ignored by git) with your configuration. See the [Configuration](#configuration) section for details on the `ProxyConfig` and `AuthConfig` structures which map to this JSON file.

**Example `demo/config.json`:**

```json
{
Expand All @@ -197,11 +207,6 @@ This repository includes a local development proxy server and a demo application
}
```

- `port`: The port the proxy server will listen on.
- `basePath`: The path prefix where the static files and proxy endpoints are served from.
- `redirectPath`: The path the proxy server intercepts for OAuth callbacks.
- `auth`: Your iRacing API credentials. Note that `tokenEndpoint` should include the `basePath`.

2. Start the proxy server:

```bash
Expand Down
64 changes: 23 additions & 41 deletions demo/index.html.template
Original file line number Diff line number Diff line change
Expand Up @@ -475,23 +475,32 @@
let currentChunkResponse = null;
let currentChunkIndex = 0;

// Check for Auth Code in URL
const params = new URLSearchParams(window.location.search);
const code = params.get('code');

if (code) {
handleCallback(code);
} else {
// Check if we are already logged in (have token)
const token = client.auth.accessToken;
if (token) {
console.log('Found existing access token, restoring session...');
showDataSection();
} else {
loginSection.style.display = 'block';
// Initialize session
async function initializeSession() {
// Show a neutral loading state or nothing while we check auth
status.innerText = 'Establishing session...';
status.style.display = 'block';

try {
const isAuthenticated = await client.auth.handleAuthentication();

status.style.display = 'none'; // Clear status

if (isAuthenticated) {
showDataSection();
} else {
loginSection.style.display = 'block';
}
} catch (err) {
console.error(err);
status.innerText = 'Authentication failed: ' + err.message;
status.style.display = 'block';
loginSection.style.display = 'block'; // Allow retry
}
}

initializeSession();

loginBtn.addEventListener('click', async () => {
try {
const url = await client.auth.generateAuthUrl();
Expand All @@ -507,33 +516,6 @@
window.location.href = window.location.pathname;
});

async function handleCallback(code) {
loginSection.style.display = 'none';
callbackSection.style.display = 'block';
status.style.display = 'block';
status.innerText = 'Exchanging code for token...';

try {
await client.auth.handleCallback(code);
status.innerText = 'Success! Token received.';

// Clear query params to clean up URL
window.history.replaceState({}, document.title, window.location.pathname);

showDataSection();
} catch (err) {
console.error(err);
let errorMessage = 'Error: ' + err.message;
if (err.body) {
const bodyStr =
typeof err.body === 'object' ? JSON.stringify(err.body, null, 2) : err.body;
errorMessage += '\n\nResponse Body:\n' + bodyStr;
}
status.innerText = errorMessage;
callbackSection.style.display = 'block';
}
}

function showDataSection() {
loginSection.style.display = 'none';
callbackSection.style.display = 'none';
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "irdata_js",
"version": "0.3.0",
"version": "0.4.0",
"description": "JavaScript library to interact with the iRacing /data API",
"type": "module",
"main": "./dist/index.cjs",
Expand Down
76 changes: 75 additions & 1 deletion src/auth/AuthManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,51 @@ export class AuthManager {
return this.tokenStore.getAccessToken();
}

/**
* Returns true if an access token is present.
*/
get isLoggedIn(): boolean {
return !!this.accessToken;
}

/**
* Comprehensive method to establish a session.
*
* Automatically handles:
* 1. Existing valid sessions (returns true immediately).
* 2. OAuth Callback handling (exchanges code for token).
* 3. Session restoration (uses Refresh Token).
*
* @returns Promise<boolean> - true if authenticated, false otherwise.
*/
async handleAuthentication(): Promise<boolean> {
// 1. Check if we already have a token
if (this.isLoggedIn) {
return true;
}

// 2. Check for OAuth callback (code in URL)
try {
await this.handleCallback();
if (this.isLoggedIn) {
// success! clean up the URL to avoid re-submitting the code on refresh
if (typeof window !== 'undefined' && window.history && window.history.replaceState) {
const url = new URL(window.location.href);
url.searchParams.delete('code');
// We also remove 'iss' or 'state' if they exist usually, but 'code' is the critical one.
window.history.replaceState({}, document.title, url.toString());
}
return true;
}
} catch (error) {
console.warn('Authentication: Callback exchange failed', error);
// Continue to try refresh token...
}

// 3. Try to refresh the session using a stored refresh token
return await this.refreshAccessToken();
}

getAuthHeaders(): HeadersInit {
const headers: HeadersInit = {};
const token = this.tokenStore.getAccessToken();
Expand Down Expand Up @@ -127,7 +172,36 @@ export class AuthManager {
return `${this.authBaseUrl}/authorize?${params.toString()}`;
}

async handleCallback(code: string): Promise<void> {
async handleCallback(codeOrUrl?: string): Promise<void> {
let input = codeOrUrl;

// In a browser, default to the current URL if no input is provided
if (!input && typeof window !== 'undefined') {
input = window.location.href;
}

if (!input) {
return;
}

let code = input;

// if the user passes a full URL, extract the code
if (input.includes('code=') || input.startsWith('http')) {
try {
const url = new URL(input);
const extractedCode = url.searchParams.get('code');
if (extractedCode) {
code = extractedCode;
} else if (input.startsWith('http')) {
// It's a URL but no code found.
return;
}
} catch {
// Not a valid URL, assume it's the code itself or handled below
}
}

let verifier = '';
if (typeof window !== 'undefined' && window.sessionStorage) {
verifier = window.sessionStorage.getItem('irdata_pkce_verifier') || '';
Expand Down
Loading