Initial commit
CI / check (push) Successful in 56s

This commit is contained in:
rudder
2026-08-12 14:02:25 +03:00
commit 3ab2d4a6cf
85 changed files with 10257 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
/**
* AuthService — device-based player authentication.
*
* Handles login via device ID (the primary auth flow for game clients),
* logout (token clearing), and auth state observation.
*/
import type { RudderContext } from '../core/context.js';
import type { RemoteConfigShape } from '../state/RemoteConfigState.js';
import { getOrCreateDeviceId } from '../device/DeviceId.js';
import { api } from '../generated/api.js';
import type { LoginViaDeviceResponse } from '../generated/auth.js';
export type AuthState = 'signed-in' | 'signed-out';
export type AuthStateListener = (state: AuthState) => void;
export interface LoginWithDeviceOptions {
/** Player's region code (default: "global"). */
region?: string;
/** Player's language code (default: "en"). */
language?: string;
/** Optional player nickname; omitted from the request when not set. */
nickname?: string;
}
export class AuthService<TConfig extends RemoteConfigShape = RemoteConfigShape> {
private readonly listeners = new Set<AuthStateListener>();
private state: AuthState;
constructor(
private readonly ctx: RudderContext,
private readonly startRuntime: () => Promise<void>,
private readonly stopRuntime: () => void,
) {
this.state = this.isAuthenticated ? 'signed-in' : 'signed-out';
}
/** True while an access token is present in the token store. */
get isAuthenticated(): boolean {
return this.ctx.options.tokenStore.getAccessToken() !== null;
}
/**
* Subscribes to auth state changes. The listener fires immediately with the
* current state (like `SyncedState.onChange`). Returns an unsubscribe function.
*/
onAuthStateChange(listener: AuthStateListener): () => void {
this.listeners.add(listener);
// Resync with the token store — tokens may have been written externally
// since construction.
this.state = this.isAuthenticated ? 'signed-in' : 'signed-out';
listener(this.state);
return () => {
this.listeners.delete(listener);
};
}
/**
* Authenticates the current device and returns access + refresh tokens.
*
* The device ID is auto-generated on first call and persisted in localStorage.
* On success, tokens are saved to the client's TokenStore.
*/
async loginWithDevice(options: LoginWithDeviceOptions = {}): Promise<LoginViaDeviceResponse> {
const { region = 'global', language = 'en', nickname } = options;
const response = await api.loginViaDevice(this.ctx, {
key: this.ctx.options.projectKey,
deviceId: getOrCreateDeviceId(),
region,
language,
nickname,
});
this.ctx.options.tokenStore.saveTokens(
response.accessToken ?? '',
response.refreshToken ?? '',
);
await this.startRuntime();
this.setState('signed-in');
return response;
}
/** Clears all stored tokens (logout). */
logout(): void {
this.ctx.options.tokenStore.clear();
this.stopRuntime();
this.setState('signed-out');
}
/**
* Called by the transport when the session is unrecoverable (refresh failed).
*
* @internal
*/
notifySessionExpired(): void {
this.setState('signed-out');
}
private setState(state: AuthState): void {
if (state === this.state) return;
this.state = state;
for (const listener of this.listeners) {
listener(state);
}
}
}