keeperboard 1.0.3 → 2.0.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/dist/index.d.mts CHANGED
@@ -1,91 +1,202 @@
1
1
  /**
2
2
  * Type definitions for KeeperBoard SDK.
3
- * Matches the API response shapes from the KeeperBoard public API.
3
+ *
4
+ * Public types use camelCase. Internal types (prefixed with Api*) match the
5
+ * snake_case shapes returned by the KeeperBoard REST API and are used only
6
+ * for deserialization inside the client.
4
7
  */
5
8
  interface KeeperBoardConfig {
6
- /** Base URL of the KeeperBoard API (e.g., "https://keeperboard.vercel.app") */
7
- apiUrl: string;
8
9
  /** API key from the KeeperBoard dashboard (e.g., "kb_dev_abc123...") */
9
10
  apiKey: string;
11
+ /** Default leaderboard name — used when no leaderboard is specified in method calls */
12
+ defaultLeaderboard?: string;
13
+ /** @internal Base URL override for testing. Do not use in production. */
14
+ apiUrl?: string;
10
15
  }
16
+ interface SubmitScoreOptions {
17
+ playerGuid: string;
18
+ playerName: string;
19
+ score: number;
20
+ /** Leaderboard name. Falls back to `defaultLeaderboard` from config. */
21
+ leaderboard?: string;
22
+ metadata?: Record<string, unknown>;
23
+ }
24
+ interface GetLeaderboardOptions {
25
+ /** Leaderboard name. Falls back to `defaultLeaderboard` from config. */
26
+ leaderboard?: string;
27
+ /** Max entries to return (1–100, default 10). */
28
+ limit?: number;
29
+ /** Offset for pagination (default 0). */
30
+ offset?: number;
31
+ /** Fetch a specific version of a time-based leaderboard. */
32
+ version?: number;
33
+ }
34
+ interface GetPlayerRankOptions {
35
+ playerGuid: string;
36
+ /** Leaderboard name. Falls back to `defaultLeaderboard` from config. */
37
+ leaderboard?: string;
38
+ }
39
+ interface UpdatePlayerNameOptions {
40
+ playerGuid: string;
41
+ newName: string;
42
+ /** Leaderboard name. Falls back to `defaultLeaderboard` from config. */
43
+ leaderboard?: string;
44
+ }
45
+ interface ClaimScoreOptions {
46
+ playerGuid: string;
47
+ playerName: string;
48
+ /** Leaderboard name. Falls back to `defaultLeaderboard` from config. */
49
+ leaderboard?: string;
50
+ }
51
+ interface ScoreResult {
52
+ id: string;
53
+ playerGuid: string;
54
+ playerName: string;
55
+ score: number;
56
+ rank: number;
57
+ isNewHighScore: boolean;
58
+ }
59
+ interface LeaderboardEntry {
60
+ rank: number;
61
+ playerGuid: string;
62
+ playerName: string;
63
+ score: number;
64
+ }
65
+ /** Reset schedule options for leaderboards */
66
+ type ResetSchedule = 'none' | 'daily' | 'weekly' | 'monthly';
67
+ interface LeaderboardResult {
68
+ entries: LeaderboardEntry[];
69
+ totalCount: number;
70
+ resetSchedule: ResetSchedule;
71
+ /** Current version number — only present when resetSchedule is not 'none'. */
72
+ version?: number;
73
+ /** Oldest available version number — only present when resetSchedule is not 'none'. */
74
+ oldestVersion?: number;
75
+ /** ISO timestamp of when the next reset occurs — only present when resetSchedule is not 'none'. */
76
+ nextReset?: string;
77
+ }
78
+ interface PlayerResult {
79
+ id: string;
80
+ playerGuid: string;
81
+ playerName: string;
82
+ score: number;
83
+ rank: number;
84
+ }
85
+ interface ClaimResult {
86
+ claimed: boolean;
87
+ score: number;
88
+ rank: number;
89
+ playerName: string;
90
+ }
91
+ interface HealthResult {
92
+ service: string;
93
+ version: string;
94
+ timestamp: string;
95
+ }
96
+ interface SessionConfig {
97
+ /** API key from the KeeperBoard dashboard */
98
+ apiKey: string;
99
+ /** Leaderboard name (required — the session is bound to one board) */
100
+ leaderboard: string;
101
+ /** PlayerIdentity config for localStorage key prefix */
102
+ identity?: {
103
+ keyPrefix?: string;
104
+ };
105
+ /** Default player name when none has been set (default: "ANON") */
106
+ defaultPlayerName?: string;
107
+ /** TTL cache configuration for getSnapshot() */
108
+ cache?: {
109
+ ttlMs: number;
110
+ };
111
+ /** Retry queue configuration for failed score submissions */
112
+ retry?: {
113
+ maxAgeMs?: number;
114
+ };
115
+ /** @internal Base URL override for testing. */
116
+ apiUrl?: string;
117
+ }
118
+ type SessionScoreResult = {
119
+ success: true;
120
+ rank: number;
121
+ isNewHighScore: boolean;
122
+ } | {
123
+ success: false;
124
+ error: string;
125
+ };
126
+ interface SnapshotEntry {
127
+ rank: number;
128
+ playerGuid: string;
129
+ playerName: string;
130
+ score: number;
131
+ isCurrentPlayer: boolean;
132
+ }
133
+ interface SnapshotResult {
134
+ entries: SnapshotEntry[];
135
+ totalCount: number;
136
+ /** Player's own rank info — included only when the player is outside the top N. */
137
+ playerRank: PlayerResult | null;
138
+ }
139
+ interface NameValidationOptions {
140
+ /** Minimum length after sanitization (default 2). */
141
+ minLength?: number;
142
+ /** Maximum length — input is truncated to this (default 12). */
143
+ maxLength?: number;
144
+ /** Convert to uppercase (default true). */
145
+ uppercase?: boolean;
146
+ /** Regex of allowed characters applied after case conversion (default /[^A-Z0-9_]/g removes non-matching). */
147
+ allowedPattern?: RegExp;
148
+ }
149
+ /** @internal */
11
150
  interface ScoreSubmission {
12
- /** Unique player identifier (UUID or custom string) */
13
151
  player_guid: string;
14
- /** Player display name */
15
152
  player_name: string;
16
- /** Score value */
17
153
  score: number;
18
- /** Optional metadata to attach to the score */
19
154
  metadata?: Record<string, unknown>;
20
155
  }
21
- /** Reset schedule options for leaderboards */
22
- type ResetSchedule = 'none' | 'daily' | 'weekly' | 'monthly';
23
- interface ScoreResponse {
24
- /** Score ID in the database */
156
+ /** @internal */
157
+ interface ApiScoreResponse {
25
158
  id: string;
26
- /** Player's unique identifier */
27
159
  player_guid: string;
28
- /** Player's display name */
29
160
  player_name: string;
30
- /** Current score value */
31
161
  score: number;
32
- /** Player's rank on the leaderboard */
33
162
  rank: number;
34
- /** Whether this submission resulted in a new high score */
35
163
  is_new_high_score: boolean;
36
164
  }
37
- interface LeaderboardEntry {
38
- /** Position on the leaderboard (1-indexed) */
165
+ /** @internal */
166
+ interface ApiLeaderboardEntry {
39
167
  rank: number;
40
- /** Player's unique identifier */
41
168
  player_guid: string;
42
- /** Player's display name */
43
169
  player_name: string;
44
- /** Score value */
45
170
  score: number;
46
171
  }
47
- interface LeaderboardResponse {
48
- /** Array of leaderboard entries */
49
- entries: LeaderboardEntry[];
50
- /** Total number of scores in this version/period */
172
+ /** @internal */
173
+ interface ApiLeaderboardResponse {
174
+ entries: ApiLeaderboardEntry[];
51
175
  total_count: number;
52
- /** The reset schedule of this leaderboard */
53
176
  reset_schedule: ResetSchedule;
54
- /** Current version number — only present when reset_schedule is not 'none' */
55
177
  version?: number;
56
- /** Oldest available version number — only present when reset_schedule is not 'none' */
57
178
  oldest_version?: number;
58
- /** ISO timestamp of when the next reset occurs — only present when reset_schedule is not 'none' */
59
179
  next_reset?: string;
60
180
  }
61
- interface PlayerResponse {
62
- /** Score ID in the database */
181
+ /** @internal */
182
+ interface ApiPlayerResponse {
63
183
  id: string;
64
- /** Player's unique identifier */
65
184
  player_guid: string;
66
- /** Player's display name */
67
185
  player_name: string;
68
- /** Player's score */
69
186
  score: number;
70
- /** Player's rank on the leaderboard */
71
187
  rank: number;
72
188
  }
73
- interface ClaimResponse {
74
- /** Whether the score was successfully claimed */
189
+ /** @internal */
190
+ interface ApiClaimResponse {
75
191
  claimed: boolean;
76
- /** The claimed score value */
77
192
  score: number;
78
- /** Player's rank after claiming */
79
193
  rank: number;
80
- /** The player name that was matched */
81
194
  player_name: string;
82
195
  }
83
- interface HealthResponse {
84
- /** Service name */
196
+ /** @internal */
197
+ interface ApiHealthResponse {
85
198
  service: string;
86
- /** API version */
87
199
  version: string;
88
- /** Server timestamp */
89
200
  timestamp: string;
90
201
  }
91
202
  declare class KeeperBoardError extends Error {
@@ -93,114 +204,155 @@ declare class KeeperBoardError extends Error {
93
204
  readonly statusCode: number;
94
205
  constructor(message: string, code: string, statusCode: number);
95
206
  }
207
+ /** @deprecated Use `ApiScoreResponse` (internal) or `ScoreResult` (public). */
208
+ type ScoreResponse = ApiScoreResponse;
209
+ /** @deprecated Use `ApiLeaderboardResponse` (internal) or `LeaderboardResult` (public). */
210
+ type LeaderboardResponse = ApiLeaderboardResponse;
211
+ /** @deprecated Use `ApiPlayerResponse` (internal) or `PlayerResult` (public). */
212
+ type PlayerResponse = ApiPlayerResponse;
213
+ /** @deprecated Use `ApiClaimResponse` (internal) or `ClaimResult` (public). */
214
+ type ClaimResponse = ApiClaimResponse;
215
+ /** @deprecated Use `ApiHealthResponse` (internal) or `HealthResult` (public). */
216
+ type HealthResponse = ApiHealthResponse;
96
217
 
97
218
  /**
98
219
  * KeeperBoard API client for TypeScript/JavaScript games.
99
220
  * Works in any browser environment using the fetch API.
221
+ *
222
+ * All public methods accept options objects and return camelCase results.
223
+ * A `defaultLeaderboard` can be set in the config to avoid passing it every call.
100
224
  */
101
225
 
102
226
  declare class KeeperBoardClient {
227
+ private static readonly DEFAULT_API_URL;
103
228
  private readonly apiUrl;
104
229
  private readonly apiKey;
230
+ private readonly defaultLeaderboard?;
105
231
  constructor(config: KeeperBoardConfig);
106
232
  /**
107
- * Submit a score to the default leaderboard.
108
- * Only updates if the new score is higher than the existing one.
109
- */
110
- submitScore(playerGuid: string, playerName: string, score: number): Promise<ScoreResponse>;
111
- /**
112
- * Submit a score to a specific leaderboard.
113
- * Only updates if the new score is higher than the existing one.
114
- */
115
- submitScore(playerGuid: string, playerName: string, score: number, leaderboard: string): Promise<ScoreResponse>;
116
- /**
117
- * Submit a score with metadata.
118
- * Only updates if the new score is higher than the existing one.
119
- */
120
- submitScore(playerGuid: string, playerName: string, score: number, leaderboard: string, metadata: Record<string, unknown>): Promise<ScoreResponse>;
121
- /**
122
- * Get the default leaderboard (top 10 entries).
233
+ * Submit a score. Only updates if the new score is higher than the existing one.
123
234
  *
124
235
  * @example
125
- * const lb = await client.getLeaderboard();
236
+ * const result = await client.submitScore({
237
+ * playerGuid: 'abc-123',
238
+ * playerName: 'ACE',
239
+ * score: 1500,
240
+ * });
241
+ * console.log(result.rank, result.isNewHighScore);
126
242
  */
127
- getLeaderboard(): Promise<LeaderboardResponse>;
243
+ submitScore(options: SubmitScoreOptions): Promise<ScoreResult>;
128
244
  /**
129
- * Get a specific leaderboard by name (top 10 entries).
245
+ * Get a leaderboard. Supports pagination and version-based lookups for
246
+ * time-based boards.
130
247
  *
131
248
  * @example
132
- * const lb = await client.getLeaderboard('Weekly Best');
133
- */
134
- getLeaderboard(name: string): Promise<LeaderboardResponse>;
135
- /**
136
- * Get a leaderboard with custom limit.
249
+ * // Top 10 on default board
250
+ * const lb = await client.getLeaderboard();
137
251
  *
138
- * @example
139
- * const lb = await client.getLeaderboard('Weekly Best', 25);
252
+ * // Top 25 on a specific board
253
+ * const lb = await client.getLeaderboard({ leaderboard: 'Weekly', limit: 25 });
254
+ *
255
+ * // Historical version
256
+ * const lb = await client.getLeaderboard({ leaderboard: 'Weekly', version: 3 });
140
257
  */
141
- getLeaderboard(name: string, limit: number): Promise<LeaderboardResponse>;
258
+ getLeaderboard(options?: GetLeaderboardOptions): Promise<LeaderboardResult>;
142
259
  /**
143
- * Get a leaderboard with pagination.
260
+ * Get a player's rank and score. Returns `null` if the player has no score.
144
261
  *
145
262
  * @example
146
- * const page2 = await client.getLeaderboard('Weekly Best', 10, 10);
263
+ * const player = await client.getPlayerRank({ playerGuid: 'abc-123' });
264
+ * if (player) console.log(`Rank #${player.rank}`);
147
265
  */
148
- getLeaderboard(name: string, limit: number, offset: number): Promise<LeaderboardResponse>;
266
+ getPlayerRank(options: GetPlayerRankOptions): Promise<PlayerResult | null>;
149
267
  /**
150
- * Get a specific version of a time-based leaderboard.
268
+ * Update a player's display name.
151
269
  *
152
270
  * @example
153
- * // Get last week's leaderboard (version 3)
154
- * const lastWeek = await client.getLeaderboardVersion('Weekly Best', 3);
271
+ * const player = await client.updatePlayerName({
272
+ * playerGuid: 'abc-123',
273
+ * newName: 'MAVERICK',
274
+ * });
155
275
  */
156
- getLeaderboardVersion(name: string, version: number): Promise<LeaderboardResponse>;
276
+ updatePlayerName(options: UpdatePlayerNameOptions): Promise<PlayerResult>;
157
277
  /**
158
- * Get a specific version with custom limit.
278
+ * Claim a migrated score by matching player name.
279
+ * Used when scores were imported without player GUIDs.
159
280
  */
160
- getLeaderboardVersion(name: string, version: number, limit: number): Promise<LeaderboardResponse>;
281
+ claimScore(options: ClaimScoreOptions): Promise<ClaimResult>;
161
282
  /**
162
- * Get a specific version with pagination.
283
+ * Check if the API is healthy. Does not require an API key.
163
284
  */
164
- getLeaderboardVersion(name: string, version: number, limit: number, offset: number): Promise<LeaderboardResponse>;
285
+ healthCheck(): Promise<HealthResult>;
286
+ private mapScoreResponse;
287
+ private mapLeaderboardResponse;
288
+ private mapPlayerResponse;
289
+ private mapClaimResponse;
290
+ private request;
291
+ }
292
+
293
+ /**
294
+ * High-level KeeperBoard API for browser games.
295
+ * Wraps KeeperBoardClient with automatic identity management, caching,
296
+ * retry queue, and name validation.
297
+ *
298
+ * Recommended for most consumers. For server-side or advanced use,
299
+ * use KeeperBoardClient directly.
300
+ */
301
+
302
+ declare class KeeperBoardSession {
303
+ private readonly client;
304
+ private readonly identity;
305
+ private readonly leaderboard;
306
+ private readonly defaultPlayerName;
307
+ private readonly cache;
308
+ private readonly retryQueue;
309
+ private isSubmitting;
310
+ constructor(config: SessionConfig);
311
+ /** Get or create a persistent player GUID. */
312
+ getPlayerGuid(): string;
313
+ /** Get the stored player name, falling back to defaultPlayerName. */
314
+ getPlayerName(): string;
315
+ /** Store a player name locally. Does NOT update the server — call updatePlayerName() for that. */
316
+ setPlayerName(name: string): void;
317
+ /** Validate a name using configurable rules. Returns sanitized string or null. */
318
+ validateName(input: string, options?: NameValidationOptions): string | null;
165
319
  /**
166
- * Get a player's rank and score on the default leaderboard.
167
- * Returns null if the player has no score.
320
+ * Submit a score. Identity and leaderboard are auto-injected.
321
+ * Returns a discriminated union: `{ success: true, rank, isNewHighScore }` or `{ success: false, error }`.
168
322
  *
169
- * @example
170
- * const player = await client.getPlayerRank(playerGuid);
171
- * if (player) {
172
- * console.log(`You are ranked #${player.rank}`);
173
- * }
174
- */
175
- getPlayerRank(playerGuid: string): Promise<PlayerResponse | null>;
176
- /**
177
- * Get a player's rank and score on a specific leaderboard.
178
- * Returns null if the player has no score.
323
+ * If retry is enabled, failed submissions are saved to localStorage for later retry.
324
+ * Prevents concurrent double-submissions.
179
325
  */
180
- getPlayerRank(playerGuid: string, leaderboard: string): Promise<PlayerResponse | null>;
326
+ submitScore(score: number, metadata?: Record<string, unknown>): Promise<SessionScoreResult>;
181
327
  /**
182
- * Update a player's display name on the default leaderboard.
183
- */
184
- updatePlayerName(playerGuid: string, newName: string): Promise<PlayerResponse>;
185
- /**
186
- * Update a player's display name on a specific leaderboard.
328
+ * Get a combined snapshot: leaderboard entries (with `isCurrentPlayer` flag)
329
+ * plus the current player's rank if they're outside the top N.
330
+ *
331
+ * Uses cache if enabled and fresh.
187
332
  */
188
- updatePlayerName(playerGuid: string, newName: string, leaderboard: string): Promise<PlayerResponse>;
333
+ getSnapshot(options?: {
334
+ limit?: number;
335
+ }): Promise<SnapshotResult>;
189
336
  /**
190
- * Claim a migrated score by matching player name.
191
- * Used when scores were imported without player GUIDs.
337
+ * Update the player's name on the server and locally.
338
+ * Returns true on success, false on failure.
192
339
  */
193
- claimScore(playerGuid: string, playerName: string): Promise<ClaimResponse>;
340
+ updatePlayerName(newName: string): Promise<boolean>;
194
341
  /**
195
- * Claim a migrated score on a specific leaderboard.
342
+ * Retry submitting a pending score (from a previous failed submission).
343
+ * Call this on app startup.
196
344
  */
197
- claimScore(playerGuid: string, playerName: string, leaderboard: string): Promise<ClaimResponse>;
345
+ retryPendingScore(): Promise<SessionScoreResult | null>;
346
+ /** Check if there's a pending score in the retry queue. */
347
+ hasPendingScore(): boolean;
198
348
  /**
199
- * Check if the API is healthy.
200
- * This endpoint does not require an API key.
349
+ * Pre-fetch snapshot data in the background for instant display later.
350
+ * No-op if cache is disabled or already fresh.
201
351
  */
202
- healthCheck(): Promise<HealthResponse>;
203
- private request;
352
+ prefetch(): void;
353
+ /** Escape hatch: access the underlying KeeperBoardClient. */
354
+ getClient(): KeeperBoardClient;
355
+ private fetchSnapshot;
204
356
  }
205
357
 
206
358
  /**
@@ -252,4 +404,78 @@ declare class PlayerIdentity {
252
404
  private generateUUID;
253
405
  }
254
406
 
255
- export { type ClaimResponse, type HealthResponse, KeeperBoardClient, type KeeperBoardConfig, KeeperBoardError, type LeaderboardEntry, type LeaderboardResponse, PlayerIdentity, type PlayerIdentityConfig, type PlayerResponse, type ResetSchedule, type ScoreResponse, type ScoreSubmission };
407
+ /**
408
+ * Pure-function name validation with configurable rules.
409
+ * Returns the sanitized name or null if invalid after sanitization.
410
+ */
411
+
412
+ /**
413
+ * Validate and sanitize a player name.
414
+ *
415
+ * 1. Trims whitespace
416
+ * 2. Optionally converts to uppercase (default: yes)
417
+ * 3. Strips characters not matching `allowedPattern`
418
+ * 4. Truncates to `maxLength`
419
+ * 5. Returns `null` if result is shorter than `minLength`
420
+ *
421
+ * @example
422
+ * validateName(' Ace Pilot! ') // 'ACE_PILOT' → wait, no spaces allowed → 'ACEPILOT'
423
+ * validateName('ab') // 'AB'
424
+ * validateName('x') // null (too short)
425
+ */
426
+ declare function validateName(input: string, options?: NameValidationOptions): string | null;
427
+
428
+ /**
429
+ * Generic TTL cache with in-flight deduplication and background refresh.
430
+ */
431
+ declare class Cache<T> {
432
+ private data;
433
+ private fetchedAt;
434
+ private inflight;
435
+ private readonly ttlMs;
436
+ constructor(ttlMs: number);
437
+ /**
438
+ * Get cached value if fresh, otherwise fetch via the provided function.
439
+ * Deduplicates concurrent calls — only one fetch runs at a time.
440
+ */
441
+ getOrFetch(fetchFn: () => Promise<T>): Promise<T>;
442
+ /**
443
+ * Trigger a background refresh without awaiting the result.
444
+ * Returns immediately. If a fetch is already in flight, does nothing.
445
+ */
446
+ refreshInBackground(fetchFn: () => Promise<T>): void;
447
+ /** Invalidate the cache, forcing the next getOrFetch to re-fetch. */
448
+ invalidate(): void;
449
+ /** Get the cached value without fetching. Returns undefined if empty or stale. */
450
+ get(): T | undefined;
451
+ /** Get the cached value even if stale. Returns undefined only if never fetched. */
452
+ getStale(): T | undefined;
453
+ /** Check if the cache has fresh (non-expired) data. */
454
+ isFresh(): boolean;
455
+ }
456
+
457
+ /**
458
+ * localStorage-based retry queue for failed score submissions.
459
+ * Persists a single pending score and auto-expires after maxAge.
460
+ */
461
+ declare class RetryQueue {
462
+ private readonly storageKey;
463
+ private readonly maxAgeMs;
464
+ constructor(storageKey: string, maxAgeMs?: number);
465
+ /** Save a failed score for later retry. */
466
+ save(score: number, metadata?: Record<string, unknown>): void;
467
+ /**
468
+ * Get the pending score, or null if none exists or it has expired.
469
+ * Automatically clears expired entries.
470
+ */
471
+ get(): {
472
+ score: number;
473
+ metadata?: Record<string, unknown>;
474
+ } | null;
475
+ /** Check if there's a pending score. */
476
+ hasPending(): boolean;
477
+ /** Clear the pending score. */
478
+ clear(): void;
479
+ }
480
+
481
+ export { Cache, type ClaimResponse, type ClaimResult, type ClaimScoreOptions, 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 UpdatePlayerNameOptions, validateName };