irdata_js 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/auth/AuthManager.d.ts +25 -0
- package/dist/auth/AuthManager.js +170 -0
- package/dist/auth/PKCEHelper.d.ts +14 -0
- package/dist/auth/PKCEHelper.js +131 -0
- package/dist/client.d.ts +28 -0
- package/dist/client.js +109 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +12 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 popmonkey
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# irdata_js
|
|
2
|
+
|
|
3
|
+
JavaScript library to interact with the iRacing /data API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install irdata_js
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
The library supports OAuth 2.0 authentication.
|
|
14
|
+
|
|
15
|
+
### 1. Initialize the Client
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
import { IRacingClient } from 'irdata_js';
|
|
19
|
+
|
|
20
|
+
const client = new IRacingClient({
|
|
21
|
+
auth: {
|
|
22
|
+
clientId: 'YOUR_CLIENT_ID', // Required for OAuth
|
|
23
|
+
redirectUri: 'YOUR_REDIRECT_URI', // Required for OAuth
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 2. Authentication
|
|
29
|
+
|
|
30
|
+
#### Web / Browser (OAuth 2.0 PKCE)
|
|
31
|
+
|
|
32
|
+
To authenticate in the browser, you need to generate an authorization URL, redirect the user, and then handle the callback.
|
|
33
|
+
|
|
34
|
+
**Step 1: Generate Auth URL and Redirect**
|
|
35
|
+
|
|
36
|
+
```javascript
|
|
37
|
+
const url = await client.auth.generateAuthUrl();
|
|
38
|
+
window.location.href = url;
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Step 2: Handle Callback**
|
|
42
|
+
|
|
43
|
+
On your redirect page, capture the `code` from the URL:
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
const params = new URLSearchParams(window.location.search);
|
|
47
|
+
const code = params.get('code');
|
|
48
|
+
|
|
49
|
+
if (code) {
|
|
50
|
+
await client.auth.handleCallback(code);
|
|
51
|
+
// Success! The client is now authenticated with an access token.
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 3. Fetch Data
|
|
56
|
+
|
|
57
|
+
Once authenticated, you can call any endpoint using `getData`. This method handles authentication headers and automatically follows S3 links if returned by the API.
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
try {
|
|
61
|
+
// Call an endpoint directly
|
|
62
|
+
const memberInfo = await client.getData('/member/info');
|
|
63
|
+
console.log(memberInfo);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error('Failed to fetch member info:', error);
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Development
|
|
70
|
+
|
|
71
|
+
### Build
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm run build
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Manual Verification (OAuth Flow)
|
|
78
|
+
|
|
79
|
+
This repository includes a local development proxy server to test the OAuth flow and API interaction, avoiding CORS issues during development.
|
|
80
|
+
|
|
81
|
+
1. Create a file named `auth_config.json` in the `examples/` directory (ignored by git) with your credentials:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"clientId": "YOUR_CLIENT_ID",
|
|
86
|
+
"redirectUri": "http://127.0.0.1/irdata_js/callback",
|
|
87
|
+
"tokenEndpoint": "http://127.0.0.1/token"
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
_Note: The `redirectUri` should match what you registered with iRacing. The proxy server is configured to intercept the path specified in `redirectUri` (e.g. `/irdata_js/callback`) and redirect it to the example app while preserving the auth code._
|
|
92
|
+
|
|
93
|
+
2. Start the proxy server:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npm run dev
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
_This starts the proxy server on port 80. Depending on your system configuration, you might need elevated privileges (e.g., `sudo`) to listen on port 80._
|
|
100
|
+
|
|
101
|
+
3. Open `http://127.0.0.1/examples/index.html` in your browser.
|
|
102
|
+
- The `index.html` is configured to use the local proxy endpoints (`/token`, `/data`, `/passthrough`) to bypass CORS restrictions enforced by the browser.
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
ISC
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
interface AuthConfig {
|
|
2
|
+
clientId?: string;
|
|
3
|
+
redirectUri?: string;
|
|
4
|
+
authBaseUrl?: string;
|
|
5
|
+
tokenEndpoint?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface TokenStore {
|
|
8
|
+
getAccessToken(): string | null;
|
|
9
|
+
setAccessToken(token: string): void;
|
|
10
|
+
getRefreshToken(): string | null;
|
|
11
|
+
setRefreshToken(token: string): void;
|
|
12
|
+
clear(): void;
|
|
13
|
+
}
|
|
14
|
+
export declare class AuthManager {
|
|
15
|
+
private tokenStore;
|
|
16
|
+
private config;
|
|
17
|
+
private baseUrl;
|
|
18
|
+
constructor(config?: AuthConfig);
|
|
19
|
+
get accessToken(): string | null;
|
|
20
|
+
getAuthHeaders(): HeadersInit;
|
|
21
|
+
generateAuthUrl(): Promise<string>;
|
|
22
|
+
handleCallback(code: string): Promise<void>;
|
|
23
|
+
refreshAccessToken(): Promise<boolean>;
|
|
24
|
+
}
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { PKCEHelper } from './PKCEHelper.js';
|
|
2
|
+
import { IRacingAPIError } from '../errors.js';
|
|
3
|
+
class InMemoryTokenStore {
|
|
4
|
+
accessToken = null;
|
|
5
|
+
refreshToken = null;
|
|
6
|
+
getAccessToken() {
|
|
7
|
+
return this.accessToken;
|
|
8
|
+
}
|
|
9
|
+
setAccessToken(token) {
|
|
10
|
+
this.accessToken = token;
|
|
11
|
+
}
|
|
12
|
+
getRefreshToken() {
|
|
13
|
+
return this.refreshToken;
|
|
14
|
+
}
|
|
15
|
+
setRefreshToken(token) {
|
|
16
|
+
this.refreshToken = token;
|
|
17
|
+
}
|
|
18
|
+
clear() {
|
|
19
|
+
this.accessToken = null;
|
|
20
|
+
this.refreshToken = null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
class LocalStorageTokenStore {
|
|
24
|
+
prefix = 'irdata_';
|
|
25
|
+
getAccessToken() {
|
|
26
|
+
return localStorage.getItem(this.prefix + 'access_token');
|
|
27
|
+
}
|
|
28
|
+
setAccessToken(token) {
|
|
29
|
+
localStorage.setItem(this.prefix + 'access_token', token);
|
|
30
|
+
}
|
|
31
|
+
getRefreshToken() {
|
|
32
|
+
return localStorage.getItem(this.prefix + 'refresh_token');
|
|
33
|
+
}
|
|
34
|
+
setRefreshToken(token) {
|
|
35
|
+
localStorage.setItem(this.prefix + 'refresh_token', token);
|
|
36
|
+
}
|
|
37
|
+
clear() {
|
|
38
|
+
localStorage.removeItem(this.prefix + 'access_token');
|
|
39
|
+
localStorage.removeItem(this.prefix + 'refresh_token');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export class AuthManager {
|
|
43
|
+
tokenStore;
|
|
44
|
+
config;
|
|
45
|
+
baseUrl = 'https://oauth.iracing.com/oauth2';
|
|
46
|
+
constructor(config = {}) {
|
|
47
|
+
this.config = config;
|
|
48
|
+
this.baseUrl = config.authBaseUrl || 'https://oauth.iracing.com/oauth2';
|
|
49
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
50
|
+
this.tokenStore = new LocalStorageTokenStore();
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
this.tokenStore = new InMemoryTokenStore();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
get accessToken() {
|
|
57
|
+
return this.tokenStore.getAccessToken();
|
|
58
|
+
}
|
|
59
|
+
getAuthHeaders() {
|
|
60
|
+
const headers = {};
|
|
61
|
+
const token = this.tokenStore.getAccessToken();
|
|
62
|
+
if (token) {
|
|
63
|
+
// OAuth2 Bearer Token
|
|
64
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
65
|
+
}
|
|
66
|
+
return headers;
|
|
67
|
+
}
|
|
68
|
+
// --- Browser OAuth2 PKCE ---
|
|
69
|
+
async generateAuthUrl() {
|
|
70
|
+
if (!this.config.clientId || !this.config.redirectUri) {
|
|
71
|
+
throw new Error('clientId and redirectUri required for OAuth');
|
|
72
|
+
}
|
|
73
|
+
const verifier = PKCEHelper.generateVerifier();
|
|
74
|
+
const challenge = await PKCEHelper.generateChallenge(verifier);
|
|
75
|
+
// Store verifier for the callback
|
|
76
|
+
if (typeof window !== 'undefined' && window.sessionStorage) {
|
|
77
|
+
window.sessionStorage.setItem('irdata_pkce_verifier', verifier);
|
|
78
|
+
}
|
|
79
|
+
const params = new URLSearchParams({
|
|
80
|
+
response_type: 'code',
|
|
81
|
+
client_id: this.config.clientId,
|
|
82
|
+
redirect_uri: this.config.redirectUri,
|
|
83
|
+
scope: 'iracing.auth', // Required based iRacing docs
|
|
84
|
+
code_challenge: challenge,
|
|
85
|
+
code_challenge_method: 'S256',
|
|
86
|
+
});
|
|
87
|
+
return `${this.baseUrl}/authorize?${params.toString()}`;
|
|
88
|
+
}
|
|
89
|
+
async handleCallback(code) {
|
|
90
|
+
let verifier = '';
|
|
91
|
+
if (typeof window !== 'undefined' && window.sessionStorage) {
|
|
92
|
+
verifier = window.sessionStorage.getItem('irdata_pkce_verifier') || '';
|
|
93
|
+
window.sessionStorage.removeItem('irdata_pkce_verifier');
|
|
94
|
+
}
|
|
95
|
+
if (!verifier) {
|
|
96
|
+
throw new Error('No PKCE verifier found');
|
|
97
|
+
}
|
|
98
|
+
const body = new URLSearchParams({
|
|
99
|
+
grant_type: 'authorization_code',
|
|
100
|
+
client_id: this.config.clientId,
|
|
101
|
+
redirect_uri: this.config.redirectUri,
|
|
102
|
+
code: code,
|
|
103
|
+
code_verifier: verifier,
|
|
104
|
+
});
|
|
105
|
+
const tokenUrl = this.config.tokenEndpoint || `${this.baseUrl}/token`;
|
|
106
|
+
const response = await fetch(tokenUrl, {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
109
|
+
body: body.toString(),
|
|
110
|
+
});
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
let errorBody;
|
|
113
|
+
try {
|
|
114
|
+
errorBody = await response.json();
|
|
115
|
+
}
|
|
116
|
+
catch (_e) {
|
|
117
|
+
try {
|
|
118
|
+
errorBody = await response.text();
|
|
119
|
+
}
|
|
120
|
+
catch (_e2) {
|
|
121
|
+
/* ignore */
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
throw new IRacingAPIError(`Failed to exchange code for token: ${response.status} ${response.statusText}`, response.status, response.statusText, errorBody);
|
|
125
|
+
}
|
|
126
|
+
const tokens = await response.json();
|
|
127
|
+
this.tokenStore.setAccessToken(tokens.access_token);
|
|
128
|
+
if (tokens.refresh_token) {
|
|
129
|
+
this.tokenStore.setRefreshToken(tokens.refresh_token);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async refreshAccessToken() {
|
|
133
|
+
const refreshToken = this.tokenStore.getRefreshToken();
|
|
134
|
+
if (!refreshToken) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
if (!this.config.clientId) {
|
|
138
|
+
throw new Error('clientId required for token refresh');
|
|
139
|
+
}
|
|
140
|
+
const body = new URLSearchParams({
|
|
141
|
+
grant_type: 'refresh_token',
|
|
142
|
+
client_id: this.config.clientId,
|
|
143
|
+
refresh_token: refreshToken,
|
|
144
|
+
});
|
|
145
|
+
const tokenUrl = this.config.tokenEndpoint || `${this.baseUrl}/token`;
|
|
146
|
+
try {
|
|
147
|
+
const response = await fetch(tokenUrl, {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
150
|
+
body: body.toString(),
|
|
151
|
+
});
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
// If refresh fails (e.g. token expired), clear tokens to force re-login
|
|
154
|
+
this.tokenStore.clear();
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
const tokens = await response.json();
|
|
158
|
+
this.tokenStore.setAccessToken(tokens.access_token);
|
|
159
|
+
// Update refresh token if a new one is returned
|
|
160
|
+
if (tokens.refresh_token) {
|
|
161
|
+
this.tokenStore.setRefreshToken(tokens.refresh_token);
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
console.error('Error refreshing token:', error);
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare class PKCEHelper {
|
|
2
|
+
/**
|
|
3
|
+
* Generates a random string for the code verifier.
|
|
4
|
+
* @param length Length of the string (43-128 characters recommended)
|
|
5
|
+
*/
|
|
6
|
+
static generateVerifier(length?: number): string;
|
|
7
|
+
/**
|
|
8
|
+
* Generates the code challenge from the verifier using SHA-256.
|
|
9
|
+
* @param verifier The code verifier string
|
|
10
|
+
*/
|
|
11
|
+
static generateChallenge(verifier: string): Promise<string>;
|
|
12
|
+
private static base64URLEncode;
|
|
13
|
+
private static sha256;
|
|
14
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
export class PKCEHelper {
|
|
2
|
+
/**
|
|
3
|
+
* Generates a random string for the code verifier.
|
|
4
|
+
* @param length Length of the string (43-128 characters recommended)
|
|
5
|
+
*/
|
|
6
|
+
static generateVerifier(length = 128) {
|
|
7
|
+
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
|
|
8
|
+
let result = '';
|
|
9
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
10
|
+
const values = new Uint8Array(length);
|
|
11
|
+
crypto.getRandomValues(values);
|
|
12
|
+
for (let i = 0; i < length; i++) {
|
|
13
|
+
result += charset[values[i] % charset.length];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
// Fallback for environments without crypto.getRandomValues (though Node 18+ and browsers have it)
|
|
18
|
+
for (let i = 0; i < length; i++) {
|
|
19
|
+
result += charset.charAt(Math.floor(Math.random() * charset.length));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Generates the code challenge from the verifier using SHA-256.
|
|
26
|
+
* @param verifier The code verifier string
|
|
27
|
+
*/
|
|
28
|
+
static async generateChallenge(verifier) {
|
|
29
|
+
const encoder = new TextEncoder();
|
|
30
|
+
const data = encoder.encode(verifier);
|
|
31
|
+
if (typeof crypto !== 'undefined' && crypto.subtle) {
|
|
32
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
33
|
+
return this.base64URLEncode(hashBuffer);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
// Fallback for insecure contexts or environments without crypto.subtle
|
|
37
|
+
const hashBuffer = this.sha256(data);
|
|
38
|
+
return this.base64URLEncode(hashBuffer);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
static base64URLEncode(buffer) {
|
|
42
|
+
const bytes = new Uint8Array(buffer);
|
|
43
|
+
let binary = '';
|
|
44
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
45
|
+
binary += String.fromCharCode(bytes[i]);
|
|
46
|
+
}
|
|
47
|
+
const base64 = btoa(binary);
|
|
48
|
+
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
49
|
+
}
|
|
50
|
+
static sha256(data) {
|
|
51
|
+
const K = [
|
|
52
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
|
|
53
|
+
0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
|
|
54
|
+
0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
|
|
55
|
+
0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
|
|
56
|
+
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
|
|
57
|
+
0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
|
|
58
|
+
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
|
|
59
|
+
0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
60
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
|
|
61
|
+
0xc67178f2,
|
|
62
|
+
];
|
|
63
|
+
function rotr(n, x) {
|
|
64
|
+
return (x >>> n) | (x << (32 - n));
|
|
65
|
+
}
|
|
66
|
+
function ch(x, y, z) {
|
|
67
|
+
return (x & y) ^ (~x & z);
|
|
68
|
+
}
|
|
69
|
+
function maj(x, y, z) {
|
|
70
|
+
return (x & y) ^ (x & z) ^ (y & z);
|
|
71
|
+
}
|
|
72
|
+
function sigma0(x) {
|
|
73
|
+
return rotr(2, x) ^ rotr(13, x) ^ rotr(22, x);
|
|
74
|
+
}
|
|
75
|
+
function sigma1(x) {
|
|
76
|
+
return rotr(6, x) ^ rotr(11, x) ^ rotr(25, x);
|
|
77
|
+
}
|
|
78
|
+
function gamma0(x) {
|
|
79
|
+
return rotr(7, x) ^ rotr(18, x) ^ (x >>> 3);
|
|
80
|
+
}
|
|
81
|
+
function gamma1(x) {
|
|
82
|
+
return rotr(17, x) ^ rotr(19, x) ^ (x >>> 10);
|
|
83
|
+
}
|
|
84
|
+
const bytes = new Uint8Array(data);
|
|
85
|
+
const len = bytes.length * 8;
|
|
86
|
+
// Padding
|
|
87
|
+
const paddingLen = (((len + 64) >>> 9) << 4) + 16;
|
|
88
|
+
const words = new Uint32Array(paddingLen);
|
|
89
|
+
for (let i = 0; i < bytes.length; i++)
|
|
90
|
+
words[i >>> 2] |= bytes[i] << (24 - (i % 4) * 8);
|
|
91
|
+
words[bytes.length >>> 2] |= 0x80 << (24 - (bytes.length % 4) * 8);
|
|
92
|
+
words[paddingLen - 1] = len;
|
|
93
|
+
const H = [
|
|
94
|
+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
|
|
95
|
+
0x5be0cd19,
|
|
96
|
+
];
|
|
97
|
+
const W = new Uint32Array(64);
|
|
98
|
+
for (let i = 0; i < words.length; i += 16) {
|
|
99
|
+
W.fill(0);
|
|
100
|
+
for (let j = 0; j < 16; j++)
|
|
101
|
+
W[j] = words[i + j];
|
|
102
|
+
for (let j = 16; j < 64; j++)
|
|
103
|
+
W[j] = (gamma1(W[j - 2]) + W[j - 7] + gamma0(W[j - 15]) + W[j - 16]) | 0;
|
|
104
|
+
let [a, b, c, d, e, f, g, h] = H;
|
|
105
|
+
for (let j = 0; j < 64; j++) {
|
|
106
|
+
const T1 = (h + sigma1(e) + ch(e, f, g) + K[j] + W[j]) | 0;
|
|
107
|
+
const T2 = (sigma0(a) + maj(a, b, c)) | 0;
|
|
108
|
+
h = g;
|
|
109
|
+
g = f;
|
|
110
|
+
f = e;
|
|
111
|
+
e = (d + T1) | 0;
|
|
112
|
+
d = c;
|
|
113
|
+
c = b;
|
|
114
|
+
b = a;
|
|
115
|
+
a = (T1 + T2) | 0;
|
|
116
|
+
}
|
|
117
|
+
H[0] = (H[0] + a) | 0;
|
|
118
|
+
H[1] = (H[1] + b) | 0;
|
|
119
|
+
H[2] = (H[2] + c) | 0;
|
|
120
|
+
H[3] = (H[3] + d) | 0;
|
|
121
|
+
H[4] = (H[4] + e) | 0;
|
|
122
|
+
H[5] = (H[5] + f) | 0;
|
|
123
|
+
H[6] = (H[6] + g) | 0;
|
|
124
|
+
H[7] = (H[7] + h) | 0;
|
|
125
|
+
}
|
|
126
|
+
const buffer = new ArrayBuffer(32);
|
|
127
|
+
const view = new DataView(buffer);
|
|
128
|
+
H.forEach((h, i) => view.setUint32(i * 4, h, false));
|
|
129
|
+
return buffer;
|
|
130
|
+
}
|
|
131
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { AuthManager } from './auth/AuthManager.js';
|
|
2
|
+
interface ClientConfig {
|
|
3
|
+
apiUrl?: string;
|
|
4
|
+
fileProxyUrl?: string;
|
|
5
|
+
auth?: {
|
|
6
|
+
clientId?: string;
|
|
7
|
+
redirectUri?: string;
|
|
8
|
+
authBaseUrl?: string;
|
|
9
|
+
tokenEndpoint?: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export declare class IRacingClient {
|
|
13
|
+
auth: AuthManager;
|
|
14
|
+
private apiUrl;
|
|
15
|
+
private fileProxyUrl?;
|
|
16
|
+
constructor(config?: ClientConfig);
|
|
17
|
+
/**
|
|
18
|
+
* Performs a fetch request with authentication headers.
|
|
19
|
+
* Does NOT automatically follow "link" responses.
|
|
20
|
+
*/
|
|
21
|
+
request<T>(endpoint: string, options?: RequestInit): Promise<T>;
|
|
22
|
+
private handleErrorResponse;
|
|
23
|
+
/**
|
|
24
|
+
* Fetches data from an endpoint, automatically following any S3 links returned.
|
|
25
|
+
*/
|
|
26
|
+
getData<T>(endpoint: string): Promise<T>;
|
|
27
|
+
}
|
|
28
|
+
export {};
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { AuthManager } from './auth/AuthManager.js';
|
|
2
|
+
import { IRacingAPIError } from './errors.js';
|
|
3
|
+
export class IRacingClient {
|
|
4
|
+
auth;
|
|
5
|
+
apiUrl;
|
|
6
|
+
fileProxyUrl;
|
|
7
|
+
constructor(config = {}) {
|
|
8
|
+
this.apiUrl = config.apiUrl || 'https://members-ng.iracing.com/data';
|
|
9
|
+
this.fileProxyUrl = config.fileProxyUrl;
|
|
10
|
+
this.auth = new AuthManager(config.auth);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Performs a fetch request with authentication headers.
|
|
14
|
+
* Does NOT automatically follow "link" responses.
|
|
15
|
+
*/
|
|
16
|
+
async request(endpoint, options = {}) {
|
|
17
|
+
// Remove leading slash from endpoint if present to avoid double slashes if apiUrl has trailing slash
|
|
18
|
+
const cleanEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint;
|
|
19
|
+
const cleanApiUrl = this.apiUrl.endsWith('/') ? this.apiUrl : `${this.apiUrl}/`;
|
|
20
|
+
const url = `${cleanApiUrl}${cleanEndpoint}`;
|
|
21
|
+
const headers = this.auth.getAuthHeaders();
|
|
22
|
+
const mergedOptions = {
|
|
23
|
+
...options,
|
|
24
|
+
headers: {
|
|
25
|
+
...headers,
|
|
26
|
+
...options.headers,
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
const response = await fetch(url, mergedOptions);
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
if (response.status === 401) {
|
|
33
|
+
// Try to refresh token
|
|
34
|
+
try {
|
|
35
|
+
const refreshed = await this.auth.refreshAccessToken();
|
|
36
|
+
if (refreshed) {
|
|
37
|
+
// Retry request with new token
|
|
38
|
+
const newHeaders = this.auth.getAuthHeaders();
|
|
39
|
+
const retryOptions = {
|
|
40
|
+
...options,
|
|
41
|
+
headers: {
|
|
42
|
+
...newHeaders,
|
|
43
|
+
...options.headers,
|
|
44
|
+
'Content-Type': 'application/json',
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
const retryResponse = await fetch(url, retryOptions);
|
|
48
|
+
if (retryResponse.ok) {
|
|
49
|
+
return retryResponse.json();
|
|
50
|
+
}
|
|
51
|
+
// If retry fails, use the retryResponse for error handling below
|
|
52
|
+
return this.handleErrorResponse(retryResponse);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (refreshError) {
|
|
56
|
+
console.error('Token refresh failed during request retry:', refreshError);
|
|
57
|
+
// Fall through to original 401 error handling
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return this.handleErrorResponse(response);
|
|
61
|
+
}
|
|
62
|
+
return response.json();
|
|
63
|
+
}
|
|
64
|
+
async handleErrorResponse(response) {
|
|
65
|
+
let body;
|
|
66
|
+
const contentType = response.headers.get('content-type');
|
|
67
|
+
try {
|
|
68
|
+
if (contentType && contentType.includes('application/json')) {
|
|
69
|
+
body = await response.json();
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
body = await response.text();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (_e) {
|
|
76
|
+
// ignore
|
|
77
|
+
}
|
|
78
|
+
throw new IRacingAPIError(`API Request failed: ${response.status} ${response.statusText}`, response.status, response.statusText, body);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Fetches data from an endpoint, automatically following any S3 links returned.
|
|
82
|
+
*/
|
|
83
|
+
async getData(endpoint) {
|
|
84
|
+
const data = await this.request(endpoint);
|
|
85
|
+
// Check if the response contains a generic link to S3 and follow it
|
|
86
|
+
if (data &&
|
|
87
|
+
typeof data === 'object' &&
|
|
88
|
+
'link' in data &&
|
|
89
|
+
typeof data.link === 'string') {
|
|
90
|
+
const s3Link = data.link;
|
|
91
|
+
if (s3Link.startsWith('http')) {
|
|
92
|
+
// Fetch the S3 link without original auth headers
|
|
93
|
+
let fetchUrl = s3Link;
|
|
94
|
+
if (this.fileProxyUrl) {
|
|
95
|
+
// If a file proxy is configured, use it
|
|
96
|
+
// e.g. http://localhost:80/passthrough?url=...
|
|
97
|
+
const separator = this.fileProxyUrl.includes('?') ? '&' : '?';
|
|
98
|
+
fetchUrl = `${this.fileProxyUrl}${separator}url=${encodeURIComponent(s3Link)}`;
|
|
99
|
+
}
|
|
100
|
+
const linkResponse = await fetch(fetchUrl);
|
|
101
|
+
if (!linkResponse.ok) {
|
|
102
|
+
return this.handleErrorResponse(linkResponse);
|
|
103
|
+
}
|
|
104
|
+
return linkResponse.json();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return data;
|
|
108
|
+
}
|
|
109
|
+
}
|
package/dist/errors.d.ts
ADDED
package/dist/errors.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export class IRacingAPIError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
statusText;
|
|
4
|
+
body;
|
|
5
|
+
constructor(message, status, statusText, body) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.statusText = statusText;
|
|
9
|
+
this.body = body;
|
|
10
|
+
this.name = 'IRacingAPIError';
|
|
11
|
+
}
|
|
12
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "irdata_js",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "JavaScript library to interact with the iRacing /data API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"dev": "node proxy_server.js",
|
|
18
|
+
"lint": "eslint .",
|
|
19
|
+
"lint:fix": "eslint . --fix",
|
|
20
|
+
"format": "prettier --write .",
|
|
21
|
+
"prepublishOnly": "npm test && npm run build"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/popmonkey/irdata_js.git"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"iracing",
|
|
29
|
+
"api",
|
|
30
|
+
"data",
|
|
31
|
+
"sdk"
|
|
32
|
+
],
|
|
33
|
+
"author": "",
|
|
34
|
+
"license": "ISC",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/popmonkey/irdata_js/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/popmonkey/irdata_js#readme",
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@eslint/js": "^9.39.2",
|
|
41
|
+
"@types/node": "^22.0.0",
|
|
42
|
+
"@typescript-eslint/eslint-plugin": "^8.53.1",
|
|
43
|
+
"@typescript-eslint/parser": "^8.53.1",
|
|
44
|
+
"cors": "^2.8.5",
|
|
45
|
+
"eslint": "^9.39.2",
|
|
46
|
+
"eslint-config-prettier": "^10.1.8",
|
|
47
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
48
|
+
"express": "^5.2.1",
|
|
49
|
+
"globals": "^17.0.0",
|
|
50
|
+
"happy-dom": "^20.1.0",
|
|
51
|
+
"prettier": "^3.8.0",
|
|
52
|
+
"typescript": "^5.0.0",
|
|
53
|
+
"typescript-eslint": "^8.53.1",
|
|
54
|
+
"vitest": "^3.2.4"
|
|
55
|
+
}
|
|
56
|
+
}
|