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 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
+ }
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ export declare class IRacingAPIError extends Error {
2
+ status: number;
3
+ statusText: string;
4
+ body?: unknown | undefined;
5
+ constructor(message: string, status: number, statusText: string, body?: unknown | undefined);
6
+ }
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
+ }
@@ -0,0 +1,4 @@
1
+ export * from './client.js';
2
+ export * from './auth/AuthManager.js';
3
+ export * from './auth/PKCEHelper.js';
4
+ export * from './errors.js';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './client.js';
2
+ export * from './auth/AuthManager.js';
3
+ export * from './auth/PKCEHelper.js';
4
+ export * from './errors.js';
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
+ }