feat(auth): Google/Apple login, named custom providers, identity link/unlink
CI / check (push) Successful in 16s
CI / publish (push) Has been skipped

This commit is contained in:
edmand46
2026-09-08 16:05:00 +03:00
parent 7271874cac
commit f45261439f
8 changed files with 293 additions and 10 deletions
+126 -4
View File
@@ -1,15 +1,21 @@
/**
* AuthService — player authentication.
*
* Handles login via device ID (the primary auth flow for game clients) or a
* custom backend webhook, logout (token clearing), and auth state observation.
* Handles login via device ID (the primary auth flow for game clients), Google,
* Apple, or a custom backend webhook, identity linking/unlinking, 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 { LoginViaCustomResponse, LoginViaDeviceResponse } from '../generated/auth.js';
import type {
LoginViaAppleResponse,
LoginViaCustomResponse,
LoginViaDeviceResponse,
LoginViaGoogleResponse,
} from '../generated/auth.js';
export type AuthState = 'signed-in' | 'signed-out';
@@ -27,6 +33,8 @@ export interface LoginWithDeviceOptions {
export interface LoginWithCustomOptions {
/** Arbitrary payload forwarded to the developer's custom auth webhook. */
customData: Record<string, unknown>;
/** Name of the custom auth provider; omitted means the default provider. */
provider?: string;
/** Player's region code (default: "global"). */
region?: string;
/** Player's language code (default: "en"). */
@@ -35,6 +43,35 @@ export interface LoginWithCustomOptions {
nickname?: string;
}
export interface LoginWithGoogleOptions {
/** Google ID token obtained from the Google sign-in flow. */
idToken: string;
/** 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 interface LoginWithAppleOptions {
/** Apple ID token (identity token) obtained from Sign in with Apple. */
idToken: string;
/** 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 interface LinkWithCustomOptions {
/** Arbitrary payload forwarded to the developer's custom auth webhook. */
customData: Record<string, unknown>;
/** Name of the custom auth provider; omitted means the default provider. */
provider?: string;
}
export class AuthService<TConfig extends RemoteConfigShape = RemoteConfigShape> {
private readonly listeners = new Set<AuthStateListener>();
private state: AuthState;
@@ -100,10 +137,11 @@ export class AuthService<TConfig extends RemoteConfigShape = RemoteConfigShape>
* saved to the client's TokenStore.
*/
async loginWithCustom(options: LoginWithCustomOptions): Promise<LoginViaCustomResponse> {
const { customData, region = 'global', language = 'en', nickname } = options;
const { customData, provider, region = 'global', language = 'en', nickname } = options;
const response = await api.loginViaCustom(this.ctx, {
key: this.ctx.options.projectKey,
customData,
provider,
region,
language,
nickname,
@@ -118,6 +156,90 @@ export class AuthService<TConfig extends RemoteConfigShape = RemoteConfigShape>
return response;
}
/**
* Authenticates with a Google ID token and returns access + refresh tokens.
*
* On success, tokens are saved to the client's TokenStore.
*/
async loginWithGoogle(options: LoginWithGoogleOptions): Promise<LoginViaGoogleResponse> {
const { idToken, region = 'global', language = 'en', nickname } = options;
const response = await api.loginViaGoogle(this.ctx, {
key: this.ctx.options.projectKey,
idToken,
region,
language,
nickname,
});
this.ctx.options.tokenStore.saveTokens(
response.accessToken ?? '',
response.refreshToken ?? '',
);
await this.startRuntime();
this.setState('signed-in');
return response;
}
/**
* Authenticates with an Apple ID token and returns access + refresh tokens.
*
* On success, tokens are saved to the client's TokenStore.
*/
async loginWithApple(options: LoginWithAppleOptions): Promise<LoginViaAppleResponse> {
const { idToken, region = 'global', language = 'en', nickname } = options;
const response = await api.loginViaApple(this.ctx, {
key: this.ctx.options.projectKey,
idToken,
region,
language,
nickname,
});
this.ctx.options.tokenStore.saveTokens(
response.accessToken ?? '',
response.refreshToken ?? '',
);
await this.startRuntime();
this.setState('signed-in');
return response;
}
/**
* Links a Google identity to the currently signed-in player. Requires an
* active session (the access token is sent automatically).
*/
async linkWithGoogle(idToken: string): Promise<void> {
await api.linkViaGoogle(this.ctx, { idToken });
}
/**
* Links an Apple identity to the currently signed-in player. Requires an
* active session (the access token is sent automatically).
*/
async linkWithApple(idToken: string): Promise<void> {
await api.linkViaApple(this.ctx, { idToken });
}
/**
* Links a custom identity to the currently signed-in player via the project's
* custom authorization webhook. Requires an active session.
*/
async linkWithCustom(options: LinkWithCustomOptions): Promise<void> {
await api.linkViaCustom(this.ctx, {
customData: options.customData,
provider: options.provider,
});
}
/**
* Unlinks an identity from the currently signed-in player. The provider is
* the full provider string: `device`, `google`, `apple`, `custom`, or
* `custom:<name>`.
*/
async unlinkIdentity(provider: string): Promise<void> {
await api.unlinkIdentity(this.ctx, { provider });
}
/** Clears all stored tokens (logout). */
logout(): void {
this.ctx.options.tokenStore.clear();