Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ dist

# Default cookie-store from `iracing-data` CLI
packages/cli/bin/*.json
examples/oauth-example-cli/credentials.json

# SEA build directories
build/
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,29 @@ A monorepo of TypeScript packages for working with the iRacing `/data` API and t
## Packages

### API

- [@iracing-data/api-schema](packages/api/schema/README.md) – Zod schemas for `/data` endpoints.
- [@iracing-data/api-schema-to-openapi](packages/helpers/api-schema-to-openapi/README.md) – Generate OpenAPI docs from the schemas.
- [@iracing-data/api-client-fetch](packages/api/client/fetch/README.md) – Fetch-based API client.
- [@iracing-data/api-client-axios](packages/api/client/axios/README.md) – Axios-based API client.
- [@iracing-data/api-router](packages/api/router/README.md) – Better Call router bundling generated routes.

### OAuth

- [@iracing-data/oauth-schema](packages/oauth/schema/README.md) – OAuth request/response Zod schemas.
- [@iracing-data/oauth-schema-to-openapi](packages/helpers/oauth-schema-to-openapi/README.md) – OpenAPI generation from OAuth schemas.
- [@iracing-data/oauth-client](packages/oauth/client/README.md) – OAuth client implementation.

### Telemetry

- [@iracing-data/telemetry-types](packages/telemetry/types/README.md) – Generated telemetry TypeScript types.
- [@iracing-data/telemetry-client-grpc-node](packages/telemetry/client/grpc-node/README.md) – Node gRPC telemetry client.
- [@iracing-data/telemetry-client-grpc-web](packages/telemetry/client/grpc-web/README.md) – Browser gRPC-Web telemetry client.
- [@iracing-data/telemetry-client-ws](packages/telemetry/client/ws/README.md) – WebSocket telemetry client.
- [@iracing-data/telemetry-client-http](packages/telemetry/client/http/README.md) – Axios HTTP telemetry client.

### Telemetry Event Emitters

- [@iracing-data/session-state-events](packages/events/session-state-events/README.md)
- [@iracing-data/track-location-events](packages/events/track-location-events/README.md)
- [@iracing-data/car-track-location-events](packages/events/car-track-location-events/README.md)
Expand All @@ -36,17 +40,24 @@ A monorepo of TypeScript packages for working with the iRacing `/data` API and t
- [@iracing-data/driver-swap-events](packages/events/driver-swap-events/README.md)

### Helpers

- [@iracing-data/helpers/sync-car-assets](packages/helpers/sync-car-assets/README.md)
- [@iracing-data/helpers/sync-track-assets](packages/helpers/sync-track-assets/README.md)
- [@iracing-data/helpers/sync-telemetry-json-schema](packages/helpers/sync-telemetry-json-schema/README.md)
- [@iracing-data/helpers/iracing-json-schema-to-typescript](packages/helpers/iracing-json-schema-to-typescript/README.md)

## Apps

- [race-events](apps/race-events/README.md) – Example CLI that logs race events from telemetry.
- [@iracing-data/sync-car-assets-cli](apps/sync-car-assets-cli/README.md) – Download car assets via CLI.
- [@iracing-data/sync-track-assets-cli](apps/sync-track-assets-cli/README.md) – Download track assets via CLI.

## Examples

See [examples/README.md](./examples/README.md).

## Rust

- [iracing-data-api-client](crates/iracing-data-api-client/README.md) – Generated Rust client for the iRacing `/data` API. Use `cargo get-member --access-token "$IRACING_ACCESS_TOKEN" --customer-ids 378767 --include-licenses` to run the member lookup example.

## Development
Expand Down
10 changes: 10 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Examples

Use this directory as a table of contents for all runnable examples.

- [kapps-hello-world](./kapps-hello-world/README.md) - Connects to a Kapps telemetry WebSocket and logs sample telemetry keys.
- [oauth-example](./oauth-example/README.md) - Browser-based OAuth app with login/logout routes and cookie-backed session state.
- [oauth-example-cli](./oauth-example-cli/README.md) - CLI OAuth flow that opens browser auth and writes credentials to disk.
- [oauth-password-limited](./oauth-password-limited/README.md) - Password-limited OAuth example that fetches iRacing `/data` assets into local output files.
- [player-pit-stop-events](./player-pit-stop-events/README.md) - Streams telemetry and emits/logs player pit road, pit stall, and service lifecycle events.
- [telemetry-client](./telemetry-client/README.md) - Basic gRPC telemetry client that subscribes to updates and prints telemetry data.
31 changes: 31 additions & 0 deletions examples/oauth-example-cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# oauth-example-cli

CLI OAuth example that starts the iRacing browser authorization flow, waits for
the callback on localhost, and writes returned credentials to
`examples/oauth-example-cli/credentials.json`.

## Usage

1. Create an `.env` file in this directory with:

```bash
IRACING_AUTH_CLIENT=<your-client-id>
IRACING_AUTH_SECRET=<your-client-secret-if-required>
OAUTH_CALLBACK_PORT=4040
```

2. Run:

```bash
pnpm --filter iracing-oauth-example-cli start
```

Optional flags:

```bash
pnpm --filter iracing-oauth-example-cli start -- --callback-port 4040 --credentials-path ./credentials.json --timeout-seconds 300
```

3. Press Enter in the CLI prompt to open your browser.
4. Complete sign-in in the browser and return to the CLI.
5. Find credentials in `credentials.json`.
20 changes: 20 additions & 0 deletions examples/oauth-example-cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "iracing-oauth-example-cli",
"version": "0.0.1",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsc --build tsconfig.build.json",
"start": "tsx --tsconfig tsconfig.build.json --env-file=.env src/index.ts"
},
"dependencies": {
"@iracing-data/oauth-client": "workspace:*",
"commander": "^14.0.2",
"dotenv": "17.2.3"
},
"devDependencies": {
"@commander-js/extra-typings": "^14.0.0",
"@types/node": "^24.10.0",
"tsx": "^4.21.0"
}
}
247 changes: 247 additions & 0 deletions examples/oauth-example-cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
#!/usr/bin/env node

import { spawn } from "node:child_process";
import { writeFile } from "node:fs/promises";
import { createServer } from "node:http";
import path from "node:path";
import { stdin, stdout } from "node:process";
import { createInterface } from "node:readline/promises";
import { fileURLToPath } from "node:url";
import { Command } from "@commander-js/extra-typings";
import {
InMemoryStore,
InternalState,
IRacingOAuthTokenResponse,
OAuthClient,
} from "@iracing-data/oauth-client";
import * as dotenv from "dotenv";

dotenv.config();

const CALLBACK_PATH = "/oauth/iracing/callback";
const CALLBACK_HOST = "127.0.0.1";
const SCOPES: Array<"iracing.profile" | "iracing.auth"> = [
"iracing.profile",
"iracing.auth",
];
const SESSION_ID = "oauth-example-cli";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRootDir = path.join(__dirname, "..");
const defaultCredentialsPath = path.join(projectRootDir, "credentials.json");

function getRequiredEnv(key: string): string {
const value = process.env[key];
if (!value) {
throw new Error(`Missing required environment variable: ${key}`);
}

return value;
}

function openUrlInBrowser(url: string): Promise<void> {
return new Promise((resolve, reject) => {
const command =
process.platform === "darwin"
? "open"
: process.platform === "win32"
? "cmd"
: "xdg-open";
const args =
process.platform === "win32" ? ["/c", "start", "", url] : [url];

const child = spawn(command, args, {
detached: true,
stdio: "ignore",
});

child.on("error", reject);
child.unref();
resolve();
});
}

async function waitForOAuthCallback(
client: OAuthClient,
callbackPort: number,
credentialsPath: string,
timeoutMs: number,
): Promise<IRacingOAuthTokenResponse> {
return await new Promise((resolve, reject) => {
let isSettled = false;

const server = createServer(async (request, response) => {
try {
const requestUrl = new URL(
request.url ?? "",
`http://${CALLBACK_HOST}:${callbackPort}`,
);

if (requestUrl.pathname !== CALLBACK_PATH) {
response.statusCode = 404;
response.end("Not found.");
return;
}

const token = await client.callback(
new URLSearchParams(requestUrl.searchParams),
SESSION_ID,
);

await writeFile(
credentialsPath,
`${JSON.stringify(token, null, 2)}\n`,
{
encoding: "utf-8",
},
);

response.statusCode = 200;
response.setHeader("Content-Type", "text/html; charset=utf-8");
response.end(
"<h1>Authenticated</h1><p>You can now return to the CLI.</p>",
);

if (!isSettled) {
isSettled = true;
server.close();
resolve(token);
}
} catch (error) {
response.statusCode = 500;
response.setHeader("Content-Type", "text/html; charset=utf-8");
response.end(
"<h1>OAuth callback failed</h1><p>See CLI output for details.</p>",
);

if (!isSettled) {
isSettled = true;
server.close();
reject(error);
}
}
});

server.on("error", (error) => {
if (!isSettled) {
isSettled = true;
reject(error);
}
});

server.listen(callbackPort, CALLBACK_HOST, () => {
const timeout = setTimeout(() => {
if (!isSettled) {
isSettled = true;
server.close();
reject(
new Error(
`Timed out waiting for OAuth callback after ${timeoutMs / 1000}s.`,
),
);
}
}, timeoutMs);

server.once("close", () => {
clearTimeout(timeout);
});
});
});
}

const program = new Command("oauth-example-cli")
.description(
"CLI OAuth example for iRacing. Opens browser auth and writes credentials to disk.",
)
.option(
"--callback-port <port>",
"Local port for the OAuth callback server",
process.env.OAUTH_CALLBACK_PORT ?? "4040",
)
.option(
"--credentials-path <path>",
"Output path for credentials.json",
defaultCredentialsPath,
)
.option(
"--timeout-seconds <seconds>",
"How long to wait for callback before failing",
"300",
)
.action(async (_, command) => {
const { callbackPort, credentialsPath, timeoutSeconds } =
command.optsWithGlobals();

const resolvedPort = Number(callbackPort);
if (!Number.isInteger(resolvedPort) || resolvedPort < 1) {
throw new Error(`Invalid callback port: "${callbackPort}"`);
}

const resolvedTimeoutSeconds = Number(timeoutSeconds);
if (
!Number.isFinite(resolvedTimeoutSeconds) ||
resolvedTimeoutSeconds <= 0
) {
throw new Error(`Invalid timeout seconds: "${timeoutSeconds}"`);
}

const timeoutMs = Math.round(resolvedTimeoutSeconds * 1000);

const clientId = getRequiredEnv("IRACING_AUTH_CLIENT");
const redirectUri = `http://${CALLBACK_HOST}:${resolvedPort}${CALLBACK_PATH}`;
const clientSecret = process.env.IRACING_AUTH_SECRET;

const client = new OAuthClient({
clientMetadata: {
clientId,
clientSecret,
redirectUri,
scopes: SCOPES,
},
stateStore: new InMemoryStore<string, InternalState>(),
sessionStore: new InMemoryStore<string, IRacingOAuthTokenResponse>(),
});

const { url } = await client.authorize();

console.info("iRacing OAuth CLI Example");
console.info(`Redirect URI: ${redirectUri}`);
console.info(`Credentials path: ${credentialsPath}`);

const readline = createInterface({ input: stdin, output: stdout });
await readline.question("Press Enter to open your browser and continue...");
readline.close();

try {
await openUrlInBrowser(url.toString());
console.info("Opened browser for OAuth sign-in.");
} catch (error) {
console.warn("Could not open browser automatically.");
console.warn("Open this URL manually:");
console.warn(url.toString());
console.warn(error);
}

console.info("Waiting for OAuth callback...");
const token = await waitForOAuthCallback(
client,
resolvedPort,
credentialsPath,
timeoutMs,
);

console.info("OAuth authentication complete.");
console.info(`Access token expires in: ${token.expires_in}s`);
if (token.refresh_token_expires_in) {
console.info(
`Refresh token expires in: ${token.refresh_token_expires_in}s`,
);
}
});

program.parseAsync().catch((error) => {
console.error("OAuth CLI flow failed.");
console.error(error);
process.exitCode = 1;
});
15 changes: 15 additions & 0 deletions examples/oauth-example-cli/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"extends": "../../tsconfig/node.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"module": "preserve",
"target": "es2022",
"moduleResolution": "bundler",
"noUnusedLocals": false,
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}
Loading
Loading