keeperboard 2.1.1 → 2.2.1

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/README.md CHANGED
@@ -233,6 +233,81 @@ try {
233
233
 
234
234
  ---
235
235
 
236
+ ## Anti-Cheat Protection
237
+
238
+ KeeperBoard provides optional anti-cheat measures to prevent casual leaderboard hacking:
239
+
240
+ ### 1. HMAC Signing
241
+
242
+ When enabled, all requests are cryptographically signed to prevent tampering.
243
+
244
+ ```typescript
245
+ const session = new KeeperBoardSession({
246
+ apiKey: 'kb_prod_xxx',
247
+ leaderboard: 'main',
248
+ signingSecret: process.env.KEEPERBOARD_SIGNING_SECRET, // From dashboard
249
+ });
250
+ ```
251
+
252
+ **Setup:**
253
+ 1. Enable "HMAC Signing" in KeeperBoard dashboard
254
+ 2. Copy the signing secret
255
+ 3. Add to your game's environment variables
256
+ 4. Pass to SDK constructor
257
+
258
+ ### 2. Run Tokens
259
+
260
+ For stronger protection, use run tokens to bind scores to game sessions:
261
+
262
+ ```typescript
263
+ // When game starts
264
+ await session.startRun();
265
+
266
+ // ... player plays the game ...
267
+
268
+ // When game ends (instead of submitScore)
269
+ const result = await session.finishRun(score);
270
+ if (result.isNewHighScore) {
271
+ console.log('New high score!');
272
+ }
273
+ ```
274
+
275
+ **Server validates:**
276
+ - Run token exists and hasn't been used
277
+ - Minimum elapsed time passed (e.g., 5+ seconds)
278
+ - Score is within cap (if configured)
279
+ - Signature is valid (if signing enabled)
280
+
281
+ ### 3. Build Obfuscation
282
+
283
+ For browser games, obfuscate your production build to make reverse-engineering harder:
284
+
285
+ ```javascript
286
+ // vite.config.js
287
+ import obfuscatorPlugin from 'vite-plugin-javascript-obfuscator';
288
+
289
+ export default {
290
+ plugins: [
291
+ obfuscatorPlugin({
292
+ include: ['src/**/*.ts'],
293
+ apply: 'build',
294
+ options: {
295
+ compact: true,
296
+ controlFlowFlattening: true,
297
+ stringArray: true,
298
+ stringArrayEncoding: ['base64'],
299
+ },
300
+ }),
301
+ ],
302
+ };
303
+ ```
304
+
305
+ ### Security Model
306
+
307
+ These measures stop **casual cheaters** (DevTools interception, simple replay attacks). Determined reverse-engineers with time and skill may still find ways around them. This is an acceptable tradeoff for most indie games.
308
+
309
+ ---
310
+
236
311
  ## Phaser.js Integration
237
312
 
238
313
  ```typescript
@@ -242,6 +317,7 @@ import { KeeperBoardSession } from 'keeperboard';
242
317
  const leaderboard = new KeeperBoardSession({
243
318
  apiKey: import.meta.env.VITE_KEEPERBOARD_API_KEY,
244
319
  leaderboard: 'main',
320
+ signingSecret: import.meta.env.VITE_KEEPERBOARD_SIGNING_SECRET, // Optional
245
321
  cache: { ttlMs: 30000 },
246
322
  retry: { maxAgeMs: 86400000 },
247
323
  });
@@ -255,11 +331,20 @@ class BootScene extends Phaser.Scene {
255
331
  }
256
332
  }
257
333
 
258
- // In GameOverScene
334
+ // In GameScene - start run for anti-cheat
335
+ class GameScene extends Phaser.Scene {
336
+ async create() {
337
+ await leaderboard.startRun(); // Optional: enables run token validation
338
+ // ... game logic ...
339
+ }
340
+ }
341
+
342
+ // In GameOverScene - use finishRun if run was started
259
343
  class GameOverScene extends Phaser.Scene {
260
344
  async create() {
261
- const result = await leaderboard.submitScore(this.score);
262
- if (result.success) {
345
+ // finishRun() uses run token if active, falls back to submitScore()
346
+ const result = await leaderboard.finishRun(this.score);
347
+ if (result.isNewHighScore) {
263
348
  this.showRank(result.rank, result.isNewHighScore);
264
349
  }
265
350
 
package/dist/index.d.mts CHANGED
@@ -10,6 +10,8 @@ interface KeeperBoardConfig {
10
10
  apiKey: string;
11
11
  /** Default leaderboard name — used when no leaderboard is specified in method calls */
12
12
  defaultLeaderboard?: string;
13
+ /** Signing secret for HMAC request signing (get from dashboard when signing is enabled) */
14
+ signingSecret?: string;
13
15
  /** @internal Base URL override for testing. Do not use in production. */
14
16
  apiUrl?: string;
15
17
  }
@@ -48,6 +50,20 @@ interface ClaimScoreOptions {
48
50
  /** Leaderboard name. Falls back to `defaultLeaderboard` from config. */
49
51
  leaderboard?: string;
50
52
  }
53
+ interface StartRunOptions {
54
+ playerGuid: string;
55
+ /** Leaderboard name. Falls back to `defaultLeaderboard` from config. */
56
+ leaderboard?: string;
57
+ }
58
+ interface FinishRunOptions {
59
+ runId: string;
60
+ playerGuid: string;
61
+ playerName: string;
62
+ score: number;
63
+ /** Leaderboard name. Falls back to `defaultLeaderboard` from config. */
64
+ leaderboard?: string;
65
+ metadata?: Record<string, unknown>;
66
+ }
51
67
  interface ScoreResult {
52
68
  id: string;
53
69
  playerGuid: string;
@@ -93,6 +109,16 @@ interface HealthResult {
93
109
  version: string;
94
110
  timestamp: string;
95
111
  }
112
+ interface StartRunResult {
113
+ runId: string;
114
+ startedAt: string;
115
+ expiresAt: string;
116
+ }
117
+ interface FinishRunResult {
118
+ scoreId: string;
119
+ rank: number;
120
+ isNewHighScore: boolean;
121
+ }
96
122
  type ErrorCode = 'PROFANITY_DETECTED' | 'RATE_LIMITED' | 'INVALID_REQUEST' | 'NOT_FOUND' | 'INTERNAL_ERROR';
97
123
  interface SessionConfig {
98
124
  /** API key from the KeeperBoard dashboard */
@@ -111,6 +137,8 @@ interface SessionConfig {
111
137
  retry?: {
112
138
  maxAgeMs?: number;
113
139
  };
140
+ /** Signing secret for HMAC request signing (get from dashboard when signing is enabled) */
141
+ signingSecret?: string;
114
142
  /** @internal Base URL override for testing. */
115
143
  apiUrl?: string;
116
144
  }
@@ -233,6 +261,7 @@ declare class KeeperBoardClient {
233
261
  private readonly apiUrl;
234
262
  private readonly apiKey;
235
263
  private readonly defaultLeaderboard?;
264
+ private readonly signingSecret?;
236
265
  constructor(config: KeeperBoardConfig);
237
266
  /**
238
267
  * Submit a score. Only updates if the new score is higher than the existing one.
@@ -288,10 +317,41 @@ declare class KeeperBoardClient {
288
317
  * Check if the API is healthy. Does not require an API key.
289
318
  */
290
319
  healthCheck(): Promise<HealthResult>;
320
+ /**
321
+ * Start a game run. Use this when the leaderboard requires run tokens.
322
+ * Returns a run ID that must be passed to finishRun() when submitting the score.
323
+ *
324
+ * @example
325
+ * const run = await client.startRun({ playerGuid: 'abc-123' });
326
+ * // ... play game ...
327
+ * const result = await client.finishRun({
328
+ * runId: run.runId,
329
+ * playerGuid: 'abc-123',
330
+ * playerName: 'ACE',
331
+ * score: 1500,
332
+ * });
333
+ */
334
+ startRun(options: StartRunOptions): Promise<StartRunResult>;
335
+ /**
336
+ * Finish a game run and submit the score.
337
+ * The run must have been started with startRun() first.
338
+ *
339
+ * @example
340
+ * const result = await client.finishRun({
341
+ * runId: run.runId,
342
+ * playerGuid: 'abc-123',
343
+ * playerName: 'ACE',
344
+ * score: 1500,
345
+ * });
346
+ * console.log(result.rank, result.isNewHighScore);
347
+ */
348
+ finishRun(options: FinishRunOptions): Promise<FinishRunResult>;
291
349
  private mapScoreResponse;
292
350
  private mapLeaderboardResponse;
293
351
  private mapPlayerResponse;
294
352
  private mapClaimResponse;
353
+ private mapStartRunResponse;
354
+ private mapFinishRunResponse;
295
355
  private request;
296
356
  }
297
357
 
@@ -312,6 +372,7 @@ declare class KeeperBoardSession {
312
372
  private readonly retryQueue;
313
373
  private cachedLimit;
314
374
  private isSubmitting;
375
+ private currentRunId;
315
376
  constructor(config: SessionConfig);
316
377
  /** Get or create a persistent player GUID. */
317
378
  getPlayerGuid(): string;
@@ -331,6 +392,31 @@ declare class KeeperBoardSession {
331
392
  * Prevents concurrent double-submissions.
332
393
  */
333
394
  submitScore(score: number, metadata?: Record<string, unknown>): Promise<SessionScoreResult>;
395
+ /**
396
+ * Start a game run. Call this when a game session begins.
397
+ * For leaderboards with `require_run_token` enabled, scores must be submitted via finishRun().
398
+ *
399
+ * The run ID is stored internally and used automatically by finishRun().
400
+ *
401
+ * @example
402
+ * await session.startRun();
403
+ * // ... play game ...
404
+ * const result = await session.finishRun(1500);
405
+ */
406
+ startRun(): Promise<StartRunResult>;
407
+ /**
408
+ * Finish a game run and submit the score.
409
+ * Must be called after startRun(). Uses the stored run ID automatically.
410
+ *
411
+ * @example
412
+ * const result = await session.finishRun(1500);
413
+ * if (result.isNewHighScore) console.log('New high score!');
414
+ */
415
+ finishRun(score: number, metadata?: Record<string, unknown>): Promise<FinishRunResult>;
416
+ /** Check if there's an active run in progress. */
417
+ hasActiveRun(): boolean;
418
+ /** Get the current run ID, or null if no run is active. */
419
+ getCurrentRunId(): string | null;
334
420
  /**
335
421
  * Get a combined snapshot: leaderboard entries (with `isCurrentPlayer` flag)
336
422
  * plus the current player's rank if they're outside the top N.
@@ -519,4 +605,4 @@ declare class RetryQueue {
519
605
  clear(): void;
520
606
  }
521
607
 
522
- export { Cache, type ClaimResponse, type ClaimResult, type ClaimScoreOptions, type ErrorCode, type GetLeaderboardOptions, type GetPlayerRankOptions, type HealthResponse, type HealthResult, KeeperBoardClient, type KeeperBoardConfig, KeeperBoardError, KeeperBoardSession, type LeaderboardEntry, type LeaderboardResponse, type LeaderboardResult, type NameValidationOptions, PlayerIdentity, type PlayerIdentityConfig, type PlayerResponse, type PlayerResult, type ResetSchedule, RetryQueue, type ScoreResponse, type ScoreResult, type ScoreSubmission, type SessionConfig, type SessionScoreResult, type SnapshotEntry, type SnapshotResult, type SubmitScoreOptions, type UpdateNameResult, type UpdatePlayerNameOptions, generatePlayerName, validateName };
608
+ export { Cache, type ClaimResponse, type ClaimResult, type ClaimScoreOptions, type ErrorCode, type FinishRunOptions, type FinishRunResult, type GetLeaderboardOptions, type GetPlayerRankOptions, type HealthResponse, type HealthResult, KeeperBoardClient, type KeeperBoardConfig, KeeperBoardError, KeeperBoardSession, type LeaderboardEntry, type LeaderboardResponse, type LeaderboardResult, type NameValidationOptions, PlayerIdentity, type PlayerIdentityConfig, type PlayerResponse, type PlayerResult, type ResetSchedule, RetryQueue, type ScoreResponse, type ScoreResult, type ScoreSubmission, type SessionConfig, type SessionScoreResult, type SnapshotEntry, type SnapshotResult, type StartRunOptions, type StartRunResult, type SubmitScoreOptions, type UpdateNameResult, type UpdatePlayerNameOptions, generatePlayerName, validateName };
package/dist/index.d.ts CHANGED
@@ -10,6 +10,8 @@ interface KeeperBoardConfig {
10
10
  apiKey: string;
11
11
  /** Default leaderboard name — used when no leaderboard is specified in method calls */
12
12
  defaultLeaderboard?: string;
13
+ /** Signing secret for HMAC request signing (get from dashboard when signing is enabled) */
14
+ signingSecret?: string;
13
15
  /** @internal Base URL override for testing. Do not use in production. */
14
16
  apiUrl?: string;
15
17
  }
@@ -48,6 +50,20 @@ interface ClaimScoreOptions {
48
50
  /** Leaderboard name. Falls back to `defaultLeaderboard` from config. */
49
51
  leaderboard?: string;
50
52
  }
53
+ interface StartRunOptions {
54
+ playerGuid: string;
55
+ /** Leaderboard name. Falls back to `defaultLeaderboard` from config. */
56
+ leaderboard?: string;
57
+ }
58
+ interface FinishRunOptions {
59
+ runId: string;
60
+ playerGuid: string;
61
+ playerName: string;
62
+ score: number;
63
+ /** Leaderboard name. Falls back to `defaultLeaderboard` from config. */
64
+ leaderboard?: string;
65
+ metadata?: Record<string, unknown>;
66
+ }
51
67
  interface ScoreResult {
52
68
  id: string;
53
69
  playerGuid: string;
@@ -93,6 +109,16 @@ interface HealthResult {
93
109
  version: string;
94
110
  timestamp: string;
95
111
  }
112
+ interface StartRunResult {
113
+ runId: string;
114
+ startedAt: string;
115
+ expiresAt: string;
116
+ }
117
+ interface FinishRunResult {
118
+ scoreId: string;
119
+ rank: number;
120
+ isNewHighScore: boolean;
121
+ }
96
122
  type ErrorCode = 'PROFANITY_DETECTED' | 'RATE_LIMITED' | 'INVALID_REQUEST' | 'NOT_FOUND' | 'INTERNAL_ERROR';
97
123
  interface SessionConfig {
98
124
  /** API key from the KeeperBoard dashboard */
@@ -111,6 +137,8 @@ interface SessionConfig {
111
137
  retry?: {
112
138
  maxAgeMs?: number;
113
139
  };
140
+ /** Signing secret for HMAC request signing (get from dashboard when signing is enabled) */
141
+ signingSecret?: string;
114
142
  /** @internal Base URL override for testing. */
115
143
  apiUrl?: string;
116
144
  }
@@ -233,6 +261,7 @@ declare class KeeperBoardClient {
233
261
  private readonly apiUrl;
234
262
  private readonly apiKey;
235
263
  private readonly defaultLeaderboard?;
264
+ private readonly signingSecret?;
236
265
  constructor(config: KeeperBoardConfig);
237
266
  /**
238
267
  * Submit a score. Only updates if the new score is higher than the existing one.
@@ -288,10 +317,41 @@ declare class KeeperBoardClient {
288
317
  * Check if the API is healthy. Does not require an API key.
289
318
  */
290
319
  healthCheck(): Promise<HealthResult>;
320
+ /**
321
+ * Start a game run. Use this when the leaderboard requires run tokens.
322
+ * Returns a run ID that must be passed to finishRun() when submitting the score.
323
+ *
324
+ * @example
325
+ * const run = await client.startRun({ playerGuid: 'abc-123' });
326
+ * // ... play game ...
327
+ * const result = await client.finishRun({
328
+ * runId: run.runId,
329
+ * playerGuid: 'abc-123',
330
+ * playerName: 'ACE',
331
+ * score: 1500,
332
+ * });
333
+ */
334
+ startRun(options: StartRunOptions): Promise<StartRunResult>;
335
+ /**
336
+ * Finish a game run and submit the score.
337
+ * The run must have been started with startRun() first.
338
+ *
339
+ * @example
340
+ * const result = await client.finishRun({
341
+ * runId: run.runId,
342
+ * playerGuid: 'abc-123',
343
+ * playerName: 'ACE',
344
+ * score: 1500,
345
+ * });
346
+ * console.log(result.rank, result.isNewHighScore);
347
+ */
348
+ finishRun(options: FinishRunOptions): Promise<FinishRunResult>;
291
349
  private mapScoreResponse;
292
350
  private mapLeaderboardResponse;
293
351
  private mapPlayerResponse;
294
352
  private mapClaimResponse;
353
+ private mapStartRunResponse;
354
+ private mapFinishRunResponse;
295
355
  private request;
296
356
  }
297
357
 
@@ -312,6 +372,7 @@ declare class KeeperBoardSession {
312
372
  private readonly retryQueue;
313
373
  private cachedLimit;
314
374
  private isSubmitting;
375
+ private currentRunId;
315
376
  constructor(config: SessionConfig);
316
377
  /** Get or create a persistent player GUID. */
317
378
  getPlayerGuid(): string;
@@ -331,6 +392,31 @@ declare class KeeperBoardSession {
331
392
  * Prevents concurrent double-submissions.
332
393
  */
333
394
  submitScore(score: number, metadata?: Record<string, unknown>): Promise<SessionScoreResult>;
395
+ /**
396
+ * Start a game run. Call this when a game session begins.
397
+ * For leaderboards with `require_run_token` enabled, scores must be submitted via finishRun().
398
+ *
399
+ * The run ID is stored internally and used automatically by finishRun().
400
+ *
401
+ * @example
402
+ * await session.startRun();
403
+ * // ... play game ...
404
+ * const result = await session.finishRun(1500);
405
+ */
406
+ startRun(): Promise<StartRunResult>;
407
+ /**
408
+ * Finish a game run and submit the score.
409
+ * Must be called after startRun(). Uses the stored run ID automatically.
410
+ *
411
+ * @example
412
+ * const result = await session.finishRun(1500);
413
+ * if (result.isNewHighScore) console.log('New high score!');
414
+ */
415
+ finishRun(score: number, metadata?: Record<string, unknown>): Promise<FinishRunResult>;
416
+ /** Check if there's an active run in progress. */
417
+ hasActiveRun(): boolean;
418
+ /** Get the current run ID, or null if no run is active. */
419
+ getCurrentRunId(): string | null;
334
420
  /**
335
421
  * Get a combined snapshot: leaderboard entries (with `isCurrentPlayer` flag)
336
422
  * plus the current player's rank if they're outside the top N.
@@ -519,4 +605,4 @@ declare class RetryQueue {
519
605
  clear(): void;
520
606
  }
521
607
 
522
- export { Cache, type ClaimResponse, type ClaimResult, type ClaimScoreOptions, type ErrorCode, type GetLeaderboardOptions, type GetPlayerRankOptions, type HealthResponse, type HealthResult, KeeperBoardClient, type KeeperBoardConfig, KeeperBoardError, KeeperBoardSession, type LeaderboardEntry, type LeaderboardResponse, type LeaderboardResult, type NameValidationOptions, PlayerIdentity, type PlayerIdentityConfig, type PlayerResponse, type PlayerResult, type ResetSchedule, RetryQueue, type ScoreResponse, type ScoreResult, type ScoreSubmission, type SessionConfig, type SessionScoreResult, type SnapshotEntry, type SnapshotResult, type SubmitScoreOptions, type UpdateNameResult, type UpdatePlayerNameOptions, generatePlayerName, validateName };
608
+ export { Cache, type ClaimResponse, type ClaimResult, type ClaimScoreOptions, type ErrorCode, type FinishRunOptions, type FinishRunResult, type GetLeaderboardOptions, type GetPlayerRankOptions, type HealthResponse, type HealthResult, KeeperBoardClient, type KeeperBoardConfig, KeeperBoardError, KeeperBoardSession, type LeaderboardEntry, type LeaderboardResponse, type LeaderboardResult, type NameValidationOptions, PlayerIdentity, type PlayerIdentityConfig, type PlayerResponse, type PlayerResult, type ResetSchedule, RetryQueue, type ScoreResponse, type ScoreResult, type ScoreSubmission, type SessionConfig, type SessionScoreResult, type SnapshotEntry, type SnapshotResult, type StartRunOptions, type StartRunResult, type SubmitScoreOptions, type UpdateNameResult, type UpdatePlayerNameOptions, generatePlayerName, validateName };