keeperboard 1.0.2 → 1.0.4

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
@@ -1,13 +1,13 @@
1
1
  # KeeperBoard SDK
2
2
 
3
- TypeScript client SDK for [KeeperBoard](https://github.com/YOUR_USERNAME/keeperboard) — a free, open-source leaderboard-as-a-service for indie game developers.
3
+ TypeScript client SDK for [KeeperBoard](https://github.com/clauderoy790/keeperboard) — a free, open-source leaderboard-as-a-service for indie game developers.
4
4
 
5
5
  Works with Phaser.js, vanilla JavaScript, and any TypeScript/JavaScript game running in the browser.
6
6
 
7
7
  ## Installation
8
8
 
9
9
  ```bash
10
- npm install keeperboard-sdk
10
+ npm install keeperboard
11
11
  ```
12
12
 
13
13
  ### Alternative: Copy source directly
@@ -24,31 +24,30 @@ import { KeeperBoardClient, PlayerIdentity } from './keeperboard/index';
24
24
 
25
25
  1. Sign up at your KeeperBoard dashboard
26
26
  2. Create a game
27
- 3. Generate an API key for your environment (dev/prod)
27
+ 3. Create an environment (dev/prod)
28
+ 4. Generate an API key for that environment
28
29
 
29
30
  ### 2. Initialize the client
30
31
 
31
32
  ```typescript
32
- import { KeeperBoardClient, PlayerIdentity } from 'keeperboard-sdk';
33
+ import { KeeperBoardClient, PlayerIdentity } from 'keeperboard';
33
34
 
34
35
  // Create the API client
35
- const keeperboard = new KeeperBoardClient({
36
- apiUrl: 'https://keeperboard.vercel.app',
37
- apiKey: 'kb_dev_your_api_key_here',
36
+ const client = new KeeperBoardClient({
37
+ apiKey: 'kb_prod_your_api_key_here',
38
38
  });
39
39
 
40
40
  // Helper for persistent player identity
41
- const playerIdentity = new PlayerIdentity();
41
+ const identity = new PlayerIdentity();
42
42
  ```
43
43
 
44
44
  ### 3. Submit a score
45
45
 
46
46
  ```typescript
47
- // Get or create a persistent player GUID
48
- const playerGuid = playerIdentity.getOrCreatePlayerGuid();
47
+ const playerGuid = identity.getOrCreatePlayerGuid();
49
48
 
50
- // Submit a score (only updates if higher than existing)
51
- const result = await keeperboard.submitScore(playerGuid, 'PlayerName', 1500);
49
+ // Submit to default leaderboard
50
+ const result = await client.submitScore(playerGuid, 'PlayerName', 1500);
52
51
 
53
52
  console.log(`Rank: #${result.rank}`);
54
53
  console.log(`New high score: ${result.is_new_high_score}`);
@@ -57,13 +56,24 @@ console.log(`New high score: ${result.is_new_high_score}`);
57
56
  ### 4. Display the leaderboard
58
57
 
59
58
  ```typescript
60
- const leaderboard = await keeperboard.getLeaderboard(10);
59
+ // Get top 10
60
+ const leaderboard = await client.getLeaderboard();
61
61
 
62
62
  leaderboard.entries.forEach((entry) => {
63
63
  console.log(`#${entry.rank} ${entry.player_name}: ${entry.score}`);
64
64
  });
65
65
  ```
66
66
 
67
+ ### 5. Show player's rank (even if not in top 10)
68
+
69
+ ```typescript
70
+ const player = await client.getPlayerRank(playerGuid);
71
+
72
+ if (player && player.rank > 10) {
73
+ console.log(`You are ranked #${player.rank} with ${player.score} points`);
74
+ }
75
+ ```
76
+
67
77
  ## API Reference
68
78
 
69
79
  ### KeeperBoardClient
@@ -72,97 +82,155 @@ leaderboard.entries.forEach((entry) => {
72
82
 
73
83
  ```typescript
74
84
  const client = new KeeperBoardClient({
75
- apiUrl: string, // Your KeeperBoard API URL
76
- apiKey: string, // API key from dashboard (e.g., "kb_dev_...")
85
+ apiKey: string, // API key from dashboard
77
86
  });
78
87
  ```
79
88
 
80
- #### Methods
89
+ > **Note:** The API key determines which game and environment you're accessing. You don't need to pass environment or game IDs — they're implicit in the key.
90
+
91
+ ---
81
92
 
82
- ##### `submitScore(playerGuid, playerName, score, metadata?, leaderboardSlug?)`
93
+ ### Score Submission
83
94
 
84
- Submit a score. Only updates if the new score is higher than the existing one.
95
+ #### `submitScore(playerGuid, playerName, score)`
96
+
97
+ Submit a score to the default leaderboard. Only updates if higher than existing score.
85
98
 
86
99
  ```typescript
87
- const result = await client.submitScore(
88
- 'player-uuid-123',
89
- 'PlayerName',
90
- 2500,
91
- { level: 10, character: 'warrior' }, // optional metadata
92
- 'high-scores', // optional leaderboard slug
93
- );
94
-
95
- // Returns:
96
- // {
97
- // id: string,
98
- // player_guid: string,
99
- // player_name: string,
100
- // score: number,
101
- // rank: number,
102
- // is_new_high_score: boolean
103
- // }
100
+ const result = await client.submitScore('player-uuid', 'PlayerName', 2500);
101
+ // Returns: { id, player_guid, player_name, score, rank, is_new_high_score }
104
102
  ```
105
103
 
106
- ##### `getLeaderboard(limit?, offset?, leaderboardSlug?)`
104
+ #### `submitScore(playerGuid, playerName, score, leaderboard)`
107
105
 
108
- Get leaderboard entries with pagination.
106
+ Submit to a specific leaderboard by name.
109
107
 
110
108
  ```typescript
111
- const result = await client.getLeaderboard(
112
- 25, // limit (max 100)
113
- 0, // offset
114
- 'high-scores', // optional leaderboard slug
115
- );
116
-
117
- // Returns:
118
- // {
119
- // entries: [{ rank, player_guid, player_name, score }],
120
- // total_count: number
121
- // }
109
+ await client.submitScore('player-uuid', 'PlayerName', 2500, 'Weekly Best');
122
110
  ```
123
111
 
124
- ##### `getPlayer(playerGuid, leaderboardSlug?)`
112
+ #### `submitScore(playerGuid, playerName, score, leaderboard, metadata)`
113
+
114
+ Submit with optional metadata.
115
+
116
+ ```typescript
117
+ await client.submitScore('player-uuid', 'PlayerName', 2500, 'Weekly Best', {
118
+ level: 10,
119
+ character: 'warrior',
120
+ });
121
+ ```
122
+
123
+ ---
124
+
125
+ ### Leaderboard
126
+
127
+ #### `getLeaderboard()`
128
+
129
+ Get the default leaderboard (top 10 entries).
130
+
131
+ ```typescript
132
+ const lb = await client.getLeaderboard();
133
+ // Returns: { entries, total_count, reset_schedule }
134
+ ```
135
+
136
+ #### `getLeaderboard(name)`
137
+
138
+ Get a specific leaderboard by name.
139
+
140
+ ```typescript
141
+ const lb = await client.getLeaderboard('Weekly Best');
142
+ ```
143
+
144
+ #### `getLeaderboard(name, limit)`
145
+
146
+ Get with a custom limit (max 100).
147
+
148
+ ```typescript
149
+ const lb = await client.getLeaderboard('Weekly Best', 50);
150
+ ```
151
+
152
+ #### `getLeaderboard(name, limit, offset)`
153
+
154
+ Get with pagination.
155
+
156
+ ```typescript
157
+ // Page 2 (entries 11-20)
158
+ const lb = await client.getLeaderboard('Weekly Best', 10, 10);
159
+ ```
160
+
161
+ ---
162
+
163
+ ### Leaderboard Versions (Time-Based)
164
+
165
+ For leaderboards with reset schedules (daily/weekly/monthly), you can query historical versions.
166
+
167
+ #### `getLeaderboardVersion(name, version)`
168
+
169
+ Get a specific version of a time-based leaderboard.
170
+
171
+ ```typescript
172
+ // Get last week's scores (version 3)
173
+ const lastWeek = await client.getLeaderboardVersion('Weekly Best', 3);
174
+ ```
175
+
176
+ #### `getLeaderboardVersion(name, version, limit, offset)`
177
+
178
+ Get historical version with pagination.
179
+
180
+ ```typescript
181
+ const lb = await client.getLeaderboardVersion('Weekly Best', 3, 25, 0);
182
+ ```
183
+
184
+ ---
185
+
186
+ ### Player
187
+
188
+ #### `getPlayerRank(playerGuid)`
125
189
 
126
- Get a player's score and rank. Returns `null` if not found.
190
+ Get a player's rank and score. Returns `null` if player has no score.
127
191
 
128
192
  ```typescript
129
- const player = await client.getPlayer('player-uuid-123');
193
+ const player = await client.getPlayerRank('player-uuid');
130
194
 
131
195
  if (player) {
132
- console.log(`Score: ${player.score}, Rank: #${player.rank}`);
196
+ console.log(`Rank: #${player.rank}, Score: ${player.score}`);
133
197
  }
134
198
  ```
135
199
 
136
- ##### `updatePlayerName(playerGuid, newName, leaderboardSlug?)`
200
+ #### `getPlayerRank(playerGuid, leaderboard)`
201
+
202
+ Get player's rank on a specific leaderboard.
203
+
204
+ ```typescript
205
+ const player = await client.getPlayerRank('player-uuid', 'Weekly Best');
206
+ ```
207
+
208
+ #### `updatePlayerName(playerGuid, newName)`
137
209
 
138
210
  Update a player's display name.
139
211
 
140
212
  ```typescript
141
- const result = await client.updatePlayerName(
142
- 'player-uuid-123',
143
- 'NewPlayerName',
144
- );
213
+ await client.updatePlayerName('player-uuid', 'NewPlayerName');
145
214
  ```
146
215
 
147
- ##### `claimScore(playerGuid, playerName, leaderboardSlug?)`
216
+ ---
217
+
218
+ ### Claim (for migrated scores)
219
+
220
+ #### `claimScore(playerGuid, playerName)`
148
221
 
149
222
  Claim a migrated score by matching player name. Used when scores were imported without player GUIDs.
150
223
 
151
224
  ```typescript
152
- try {
153
- const result = await client.claimScore(
154
- 'new-player-guid',
155
- 'ImportedPlayerName',
156
- );
157
- console.log(`Claimed score: ${result.score}`);
158
- } catch (error) {
159
- if (error.code === 'NOT_FOUND') {
160
- console.log('No unclaimed score found');
161
- }
162
- }
225
+ const result = await client.claimScore('new-player-guid', 'ImportedPlayerName');
226
+ console.log(`Claimed score: ${result.score}, Rank: #${result.rank}`);
163
227
  ```
164
228
 
165
- ##### `healthCheck()`
229
+ ---
230
+
231
+ ### Health Check
232
+
233
+ #### `healthCheck()`
166
234
 
167
235
  Check if the API is healthy. Does not require an API key.
168
236
 
@@ -171,6 +239,8 @@ const health = await client.healthCheck();
171
239
  console.log(`API Version: ${health.version}`);
172
240
  ```
173
241
 
242
+ ---
243
+
174
244
  ### PlayerIdentity
175
245
 
176
246
  Helper for managing persistent player identity in localStorage.
@@ -196,12 +266,14 @@ if (identity.hasIdentity()) {
196
266
  identity.clear();
197
267
  ```
198
268
 
269
+ ---
270
+
199
271
  ### Error Handling
200
272
 
201
273
  All methods throw `KeeperBoardError` on failure:
202
274
 
203
275
  ```typescript
204
- import { KeeperBoardError } from 'keeperboard-sdk';
276
+ import { KeeperBoardError } from 'keeperboard';
205
277
 
206
278
  try {
207
279
  await client.submitScore(playerGuid, name, score);
@@ -219,25 +291,40 @@ try {
219
291
  case 'INVALID_REQUEST':
220
292
  // Check request parameters
221
293
  break;
222
- case 'ALREADY_CLAIMED':
223
- // Player already has a score
224
- break;
225
294
  }
226
295
  }
227
296
  }
228
297
  ```
229
298
 
299
+ ---
300
+
301
+ ## Multiple Leaderboards
302
+
303
+ If your game has multiple leaderboards (e.g., per-level or per-mode):
304
+
305
+ ```typescript
306
+ // Submit to different leaderboards
307
+ await client.submitScore(guid, name, score, 'Level 1');
308
+ await client.submitScore(guid, name, score, 'Endless Mode');
309
+ await client.submitScore(guid, name, score, 'Weekly Challenge');
310
+
311
+ // Get specific leaderboards
312
+ const level1 = await client.getLeaderboard('Level 1');
313
+ const endless = await client.getLeaderboard('Endless Mode', 50);
314
+ ```
315
+
316
+ ---
317
+
230
318
  ## Phaser.js Integration Example
231
319
 
232
320
  ```typescript
233
- import { KeeperBoardClient, PlayerIdentity } from 'keeperboard-sdk';
321
+ import { KeeperBoardClient, PlayerIdentity } from 'keeperboard';
234
322
 
235
- const keeperboard = new KeeperBoardClient({
236
- apiUrl: 'https://keeperboard.vercel.app',
323
+ const client = new KeeperBoardClient({
237
324
  apiKey: 'kb_prod_your_api_key',
238
325
  });
239
326
 
240
- const playerIdentity = new PlayerIdentity();
327
+ const identity = new PlayerIdentity();
241
328
 
242
329
  class GameOverScene extends Phaser.Scene {
243
330
  private score: number = 0;
@@ -247,21 +334,15 @@ class GameOverScene extends Phaser.Scene {
247
334
  }
248
335
 
249
336
  async create() {
250
- const playerGuid = playerIdentity.getOrCreatePlayerGuid();
251
- const playerName = playerIdentity.getPlayerName() ?? 'Anonymous';
337
+ const playerGuid = identity.getOrCreatePlayerGuid();
338
+ const playerName = identity.getPlayerName() ?? 'Anonymous';
252
339
 
253
340
  // Submit score
254
- const result = await keeperboard.submitScore(
255
- playerGuid,
256
- playerName,
257
- this.score,
258
- );
341
+ const result = await client.submitScore(playerGuid, playerName, this.score);
259
342
 
260
- // Display result
343
+ // Display rank
261
344
  this.add
262
- .text(400, 200, `Your Rank: #${result.rank}`, {
263
- fontSize: '32px',
264
- })
345
+ .text(400, 200, `Your Rank: #${result.rank}`, { fontSize: '32px' })
265
346
  .setOrigin(0.5);
266
347
 
267
348
  if (result.is_new_high_score) {
@@ -273,8 +354,8 @@ class GameOverScene extends Phaser.Scene {
273
354
  .setOrigin(0.5);
274
355
  }
275
356
 
276
- // Display leaderboard
277
- const leaderboard = await keeperboard.getLeaderboard(10);
357
+ // Display top 10
358
+ const leaderboard = await client.getLeaderboard();
278
359
 
279
360
  leaderboard.entries.forEach((entry, index) => {
280
361
  const isMe = entry.player_guid === playerGuid;
@@ -289,40 +370,22 @@ class GameOverScene extends Phaser.Scene {
289
370
  )
290
371
  .setOrigin(0.5);
291
372
  });
373
+
374
+ // Show player's rank if not in top 10
375
+ const player = await client.getPlayerRank(playerGuid);
376
+ if (player && player.rank > 10) {
377
+ this.add
378
+ .text(400, 660, `... #${player.rank} ${playerName}: ${player.score}`, {
379
+ fontSize: '18px',
380
+ color: '#00ff00',
381
+ })
382
+ .setOrigin(0.5);
383
+ }
292
384
  }
293
385
  }
294
386
  ```
295
387
 
296
- ## Multiple Leaderboards
297
-
298
- If your game has multiple leaderboards (e.g., per-level or per-mode), use the `leaderboardSlug` parameter:
299
-
300
- ```typescript
301
- // Submit to a specific leaderboard
302
- await client.submitScore(guid, name, score, undefined, 'level-1-scores');
303
- await client.submitScore(guid, name, score, undefined, 'endless-mode');
304
-
305
- // Get a specific leaderboard
306
- const level1 = await client.getLeaderboard(10, 0, 'level-1-scores');
307
- const endless = await client.getLeaderboard(10, 0, 'endless-mode');
308
- ```
309
-
310
- ## TypeScript Types
311
-
312
- All types are exported for your convenience:
313
-
314
- ```typescript
315
- import type {
316
- KeeperBoardConfig,
317
- ScoreSubmission,
318
- ScoreResponse,
319
- LeaderboardEntry,
320
- LeaderboardResponse,
321
- PlayerResponse,
322
- ClaimResponse,
323
- HealthResponse,
324
- } from 'keeperboard-sdk';
325
- ```
388
+ ---
326
389
 
327
390
  ## Development
328
391
 
@@ -336,10 +399,29 @@ npm run typecheck
336
399
  # Build
337
400
  npm run build
338
401
 
402
+ # Run tests (requires .env with Supabase credentials)
403
+ npm test
404
+
339
405
  # Clean
340
406
  npm run clean
341
407
  ```
342
408
 
409
+ ### Running Tests
410
+
411
+ Copy `.env.example` to `.env` and fill in your Supabase credentials:
412
+
413
+ ```bash
414
+ cp .env.example .env
415
+ ```
416
+
417
+ Then run:
418
+
419
+ ```bash
420
+ npm test
421
+ ```
422
+
423
+ ---
424
+
343
425
  ## License
344
426
 
345
427
  MIT
package/dist/index.d.mts CHANGED
@@ -3,10 +3,10 @@
3
3
  * Matches the API response shapes from the KeeperBoard public API.
4
4
  */
5
5
  interface KeeperBoardConfig {
6
- /** Base URL of the KeeperBoard API (e.g., "https://keeperboard.vercel.app") */
7
- apiUrl: string;
8
6
  /** API key from the KeeperBoard dashboard (e.g., "kb_dev_abc123...") */
9
7
  apiKey: string;
8
+ /** @internal Base URL override for testing. Do not use in production. */
9
+ apiUrl?: string;
10
10
  }
11
11
  interface ScoreSubmission {
12
12
  /** Unique player identifier (UUID or custom string) */
@@ -18,16 +18,8 @@ interface ScoreSubmission {
18
18
  /** Optional metadata to attach to the score */
19
19
  metadata?: Record<string, unknown>;
20
20
  }
21
- interface PlayerNameUpdate {
22
- /** New display name for the player */
23
- player_name: string;
24
- }
25
- interface ClaimRequest {
26
- /** Player GUID to assign to the migrated score */
27
- player_guid: string;
28
- /** Player name to match against migrated scores */
29
- player_name: string;
30
- }
21
+ /** Reset schedule options for leaderboards */
22
+ type ResetSchedule = 'none' | 'daily' | 'weekly' | 'monthly';
31
23
  interface ScoreResponse {
32
24
  /** Score ID in the database */
33
25
  id: string;
@@ -55,8 +47,16 @@ interface LeaderboardEntry {
55
47
  interface LeaderboardResponse {
56
48
  /** Array of leaderboard entries */
57
49
  entries: LeaderboardEntry[];
58
- /** Total number of scores on this leaderboard */
50
+ /** Total number of scores in this version/period */
59
51
  total_count: number;
52
+ /** The reset schedule of this leaderboard */
53
+ reset_schedule: ResetSchedule;
54
+ /** Current version number — only present when reset_schedule is not 'none' */
55
+ version?: number;
56
+ /** Oldest available version number — only present when reset_schedule is not 'none' */
57
+ oldest_version?: number;
58
+ /** ISO timestamp of when the next reset occurs — only present when reset_schedule is not 'none' */
59
+ next_reset?: string;
60
60
  }
61
61
  interface PlayerResponse {
62
62
  /** Score ID in the database */
@@ -88,16 +88,6 @@ interface HealthResponse {
88
88
  /** Server timestamp */
89
89
  timestamp: string;
90
90
  }
91
- interface ApiSuccessResponse<T> {
92
- success: true;
93
- data: T;
94
- }
95
- interface ApiErrorResponse {
96
- success: false;
97
- error: string;
98
- code: string;
99
- }
100
- type ApiResponse<T> = ApiSuccessResponse<T> | ApiErrorResponse;
101
91
  declare class KeeperBoardError extends Error {
102
92
  readonly code: string;
103
93
  readonly statusCode: number;
@@ -110,67 +100,107 @@ declare class KeeperBoardError extends Error {
110
100
  */
111
101
 
112
102
  declare class KeeperBoardClient {
103
+ private static readonly DEFAULT_API_URL;
113
104
  private readonly apiUrl;
114
105
  private readonly apiKey;
115
106
  constructor(config: KeeperBoardConfig);
116
107
  /**
117
- * Submit a score to the leaderboard.
108
+ * Submit a score to the default leaderboard.
109
+ * Only updates if the new score is higher than the existing one.
110
+ */
111
+ submitScore(playerGuid: string, playerName: string, score: number): Promise<ScoreResponse>;
112
+ /**
113
+ * Submit a score to a specific leaderboard.
118
114
  * Only updates if the new score is higher than the existing one.
115
+ */
116
+ submitScore(playerGuid: string, playerName: string, score: number, leaderboard: string): Promise<ScoreResponse>;
117
+ /**
118
+ * Submit a score with metadata.
119
+ * Only updates if the new score is higher than the existing one.
120
+ */
121
+ submitScore(playerGuid: string, playerName: string, score: number, leaderboard: string, metadata: Record<string, unknown>): Promise<ScoreResponse>;
122
+ /**
123
+ * Get the default leaderboard (top 10 entries).
119
124
  *
120
- * @param playerGuid - Unique player identifier
121
- * @param playerName - Player display name
122
- * @param score - Score value
123
- * @param metadata - Optional metadata to attach
124
- * @param leaderboardSlug - Optional leaderboard slug (uses default if not specified)
125
- * @returns Score response with rank and whether it's a new high score
125
+ * @example
126
+ * const lb = await client.getLeaderboard();
126
127
  */
127
- submitScore(playerGuid: string, playerName: string, score: number, metadata?: Record<string, unknown>, leaderboardSlug?: string): Promise<ScoreResponse>;
128
+ getLeaderboard(): Promise<LeaderboardResponse>;
128
129
  /**
129
- * Get the leaderboard entries with pagination.
130
+ * Get a specific leaderboard by name (top 10 entries).
130
131
  *
131
- * @param limit - Maximum number of entries to return (default: 10, max: 100)
132
- * @param offset - Pagination offset (default: 0)
133
- * @param leaderboardSlug - Optional leaderboard slug (uses default if not specified)
134
- * @returns Leaderboard entries with total count
132
+ * @example
133
+ * const lb = await client.getLeaderboard('Weekly Best');
135
134
  */
136
- getLeaderboard(limit?: number, offset?: number, leaderboardSlug?: string): Promise<LeaderboardResponse>;
135
+ getLeaderboard(name: string): Promise<LeaderboardResponse>;
137
136
  /**
138
- * Get a specific player's score and rank.
137
+ * Get a leaderboard with custom limit.
139
138
  *
140
- * @param playerGuid - Player's unique identifier
141
- * @param leaderboardSlug - Optional leaderboard slug (uses default if not specified)
142
- * @returns Player's score and rank, or null if not found
139
+ * @example
140
+ * const lb = await client.getLeaderboard('Weekly Best', 25);
143
141
  */
144
- getPlayer(playerGuid: string, leaderboardSlug?: string): Promise<PlayerResponse | null>;
142
+ getLeaderboard(name: string, limit: number): Promise<LeaderboardResponse>;
145
143
  /**
146
- * Update a player's display name.
144
+ * Get a leaderboard with pagination.
147
145
  *
148
- * @param playerGuid - Player's unique identifier
149
- * @param newName - New display name
150
- * @param leaderboardSlug - Optional leaderboard slug (uses default if not specified)
151
- * @returns Updated player info
146
+ * @example
147
+ * const page2 = await client.getLeaderboard('Weekly Best', 10, 10);
152
148
  */
153
- updatePlayerName(playerGuid: string, newName: string, leaderboardSlug?: string): Promise<PlayerResponse>;
149
+ getLeaderboard(name: string, limit: number, offset: number): Promise<LeaderboardResponse>;
154
150
  /**
155
- * Claim a migrated score by matching player name.
156
- * Used when scores were imported (e.g., from CSV) without player GUIDs.
151
+ * Get a specific version of a time-based leaderboard.
157
152
  *
158
- * @param playerGuid - Player GUID to assign to the claimed score
159
- * @param playerName - Player name to match against migrated scores
160
- * @param leaderboardSlug - Optional leaderboard slug (uses default if not specified)
161
- * @returns Claim result with score and rank
153
+ * @example
154
+ * // Get last week's leaderboard (version 3)
155
+ * const lastWeek = await client.getLeaderboardVersion('Weekly Best', 3);
162
156
  */
163
- claimScore(playerGuid: string, playerName: string, leaderboardSlug?: string): Promise<ClaimResponse>;
157
+ getLeaderboardVersion(name: string, version: number): Promise<LeaderboardResponse>;
164
158
  /**
165
- * Check if the API is healthy.
166
- * This endpoint does not require an API key.
159
+ * Get a specific version with custom limit.
160
+ */
161
+ getLeaderboardVersion(name: string, version: number, limit: number): Promise<LeaderboardResponse>;
162
+ /**
163
+ * Get a specific version with pagination.
164
+ */
165
+ getLeaderboardVersion(name: string, version: number, limit: number, offset: number): Promise<LeaderboardResponse>;
166
+ /**
167
+ * Get a player's rank and score on the default leaderboard.
168
+ * Returns null if the player has no score.
167
169
  *
168
- * @returns Health status with version and timestamp
170
+ * @example
171
+ * const player = await client.getPlayerRank(playerGuid);
172
+ * if (player) {
173
+ * console.log(`You are ranked #${player.rank}`);
174
+ * }
169
175
  */
170
- healthCheck(): Promise<HealthResponse>;
176
+ getPlayerRank(playerGuid: string): Promise<PlayerResponse | null>;
177
+ /**
178
+ * Get a player's rank and score on a specific leaderboard.
179
+ * Returns null if the player has no score.
180
+ */
181
+ getPlayerRank(playerGuid: string, leaderboard: string): Promise<PlayerResponse | null>;
182
+ /**
183
+ * Update a player's display name on the default leaderboard.
184
+ */
185
+ updatePlayerName(playerGuid: string, newName: string): Promise<PlayerResponse>;
186
+ /**
187
+ * Update a player's display name on a specific leaderboard.
188
+ */
189
+ updatePlayerName(playerGuid: string, newName: string, leaderboard: string): Promise<PlayerResponse>;
171
190
  /**
172
- * Internal request helper with auth and error handling.
191
+ * Claim a migrated score by matching player name.
192
+ * Used when scores were imported without player GUIDs.
173
193
  */
194
+ claimScore(playerGuid: string, playerName: string): Promise<ClaimResponse>;
195
+ /**
196
+ * Claim a migrated score on a specific leaderboard.
197
+ */
198
+ claimScore(playerGuid: string, playerName: string, leaderboard: string): Promise<ClaimResponse>;
199
+ /**
200
+ * Check if the API is healthy.
201
+ * This endpoint does not require an API key.
202
+ */
203
+ healthCheck(): Promise<HealthResponse>;
174
204
  private request;
175
205
  }
176
206
 
@@ -223,4 +253,4 @@ declare class PlayerIdentity {
223
253
  private generateUUID;
224
254
  }
225
255
 
226
- export { type ApiErrorResponse, type ApiResponse, type ApiSuccessResponse, type ClaimRequest, type ClaimResponse, type HealthResponse, KeeperBoardClient, type KeeperBoardConfig, KeeperBoardError, type LeaderboardEntry, type LeaderboardResponse, PlayerIdentity, type PlayerIdentityConfig, type PlayerNameUpdate, type PlayerResponse, type ScoreResponse, type ScoreSubmission };
256
+ export { type ClaimResponse, type HealthResponse, KeeperBoardClient, type KeeperBoardConfig, KeeperBoardError, type LeaderboardEntry, type LeaderboardResponse, PlayerIdentity, type PlayerIdentityConfig, type PlayerResponse, type ResetSchedule, type ScoreResponse, type ScoreSubmission };
package/dist/index.d.ts CHANGED
@@ -3,10 +3,10 @@
3
3
  * Matches the API response shapes from the KeeperBoard public API.
4
4
  */
5
5
  interface KeeperBoardConfig {
6
- /** Base URL of the KeeperBoard API (e.g., "https://keeperboard.vercel.app") */
7
- apiUrl: string;
8
6
  /** API key from the KeeperBoard dashboard (e.g., "kb_dev_abc123...") */
9
7
  apiKey: string;
8
+ /** @internal Base URL override for testing. Do not use in production. */
9
+ apiUrl?: string;
10
10
  }
11
11
  interface ScoreSubmission {
12
12
  /** Unique player identifier (UUID or custom string) */
@@ -18,16 +18,8 @@ interface ScoreSubmission {
18
18
  /** Optional metadata to attach to the score */
19
19
  metadata?: Record<string, unknown>;
20
20
  }
21
- interface PlayerNameUpdate {
22
- /** New display name for the player */
23
- player_name: string;
24
- }
25
- interface ClaimRequest {
26
- /** Player GUID to assign to the migrated score */
27
- player_guid: string;
28
- /** Player name to match against migrated scores */
29
- player_name: string;
30
- }
21
+ /** Reset schedule options for leaderboards */
22
+ type ResetSchedule = 'none' | 'daily' | 'weekly' | 'monthly';
31
23
  interface ScoreResponse {
32
24
  /** Score ID in the database */
33
25
  id: string;
@@ -55,8 +47,16 @@ interface LeaderboardEntry {
55
47
  interface LeaderboardResponse {
56
48
  /** Array of leaderboard entries */
57
49
  entries: LeaderboardEntry[];
58
- /** Total number of scores on this leaderboard */
50
+ /** Total number of scores in this version/period */
59
51
  total_count: number;
52
+ /** The reset schedule of this leaderboard */
53
+ reset_schedule: ResetSchedule;
54
+ /** Current version number — only present when reset_schedule is not 'none' */
55
+ version?: number;
56
+ /** Oldest available version number — only present when reset_schedule is not 'none' */
57
+ oldest_version?: number;
58
+ /** ISO timestamp of when the next reset occurs — only present when reset_schedule is not 'none' */
59
+ next_reset?: string;
60
60
  }
61
61
  interface PlayerResponse {
62
62
  /** Score ID in the database */
@@ -88,16 +88,6 @@ interface HealthResponse {
88
88
  /** Server timestamp */
89
89
  timestamp: string;
90
90
  }
91
- interface ApiSuccessResponse<T> {
92
- success: true;
93
- data: T;
94
- }
95
- interface ApiErrorResponse {
96
- success: false;
97
- error: string;
98
- code: string;
99
- }
100
- type ApiResponse<T> = ApiSuccessResponse<T> | ApiErrorResponse;
101
91
  declare class KeeperBoardError extends Error {
102
92
  readonly code: string;
103
93
  readonly statusCode: number;
@@ -110,67 +100,107 @@ declare class KeeperBoardError extends Error {
110
100
  */
111
101
 
112
102
  declare class KeeperBoardClient {
103
+ private static readonly DEFAULT_API_URL;
113
104
  private readonly apiUrl;
114
105
  private readonly apiKey;
115
106
  constructor(config: KeeperBoardConfig);
116
107
  /**
117
- * Submit a score to the leaderboard.
108
+ * Submit a score to the default leaderboard.
109
+ * Only updates if the new score is higher than the existing one.
110
+ */
111
+ submitScore(playerGuid: string, playerName: string, score: number): Promise<ScoreResponse>;
112
+ /**
113
+ * Submit a score to a specific leaderboard.
118
114
  * Only updates if the new score is higher than the existing one.
115
+ */
116
+ submitScore(playerGuid: string, playerName: string, score: number, leaderboard: string): Promise<ScoreResponse>;
117
+ /**
118
+ * Submit a score with metadata.
119
+ * Only updates if the new score is higher than the existing one.
120
+ */
121
+ submitScore(playerGuid: string, playerName: string, score: number, leaderboard: string, metadata: Record<string, unknown>): Promise<ScoreResponse>;
122
+ /**
123
+ * Get the default leaderboard (top 10 entries).
119
124
  *
120
- * @param playerGuid - Unique player identifier
121
- * @param playerName - Player display name
122
- * @param score - Score value
123
- * @param metadata - Optional metadata to attach
124
- * @param leaderboardSlug - Optional leaderboard slug (uses default if not specified)
125
- * @returns Score response with rank and whether it's a new high score
125
+ * @example
126
+ * const lb = await client.getLeaderboard();
126
127
  */
127
- submitScore(playerGuid: string, playerName: string, score: number, metadata?: Record<string, unknown>, leaderboardSlug?: string): Promise<ScoreResponse>;
128
+ getLeaderboard(): Promise<LeaderboardResponse>;
128
129
  /**
129
- * Get the leaderboard entries with pagination.
130
+ * Get a specific leaderboard by name (top 10 entries).
130
131
  *
131
- * @param limit - Maximum number of entries to return (default: 10, max: 100)
132
- * @param offset - Pagination offset (default: 0)
133
- * @param leaderboardSlug - Optional leaderboard slug (uses default if not specified)
134
- * @returns Leaderboard entries with total count
132
+ * @example
133
+ * const lb = await client.getLeaderboard('Weekly Best');
135
134
  */
136
- getLeaderboard(limit?: number, offset?: number, leaderboardSlug?: string): Promise<LeaderboardResponse>;
135
+ getLeaderboard(name: string): Promise<LeaderboardResponse>;
137
136
  /**
138
- * Get a specific player's score and rank.
137
+ * Get a leaderboard with custom limit.
139
138
  *
140
- * @param playerGuid - Player's unique identifier
141
- * @param leaderboardSlug - Optional leaderboard slug (uses default if not specified)
142
- * @returns Player's score and rank, or null if not found
139
+ * @example
140
+ * const lb = await client.getLeaderboard('Weekly Best', 25);
143
141
  */
144
- getPlayer(playerGuid: string, leaderboardSlug?: string): Promise<PlayerResponse | null>;
142
+ getLeaderboard(name: string, limit: number): Promise<LeaderboardResponse>;
145
143
  /**
146
- * Update a player's display name.
144
+ * Get a leaderboard with pagination.
147
145
  *
148
- * @param playerGuid - Player's unique identifier
149
- * @param newName - New display name
150
- * @param leaderboardSlug - Optional leaderboard slug (uses default if not specified)
151
- * @returns Updated player info
146
+ * @example
147
+ * const page2 = await client.getLeaderboard('Weekly Best', 10, 10);
152
148
  */
153
- updatePlayerName(playerGuid: string, newName: string, leaderboardSlug?: string): Promise<PlayerResponse>;
149
+ getLeaderboard(name: string, limit: number, offset: number): Promise<LeaderboardResponse>;
154
150
  /**
155
- * Claim a migrated score by matching player name.
156
- * Used when scores were imported (e.g., from CSV) without player GUIDs.
151
+ * Get a specific version of a time-based leaderboard.
157
152
  *
158
- * @param playerGuid - Player GUID to assign to the claimed score
159
- * @param playerName - Player name to match against migrated scores
160
- * @param leaderboardSlug - Optional leaderboard slug (uses default if not specified)
161
- * @returns Claim result with score and rank
153
+ * @example
154
+ * // Get last week's leaderboard (version 3)
155
+ * const lastWeek = await client.getLeaderboardVersion('Weekly Best', 3);
162
156
  */
163
- claimScore(playerGuid: string, playerName: string, leaderboardSlug?: string): Promise<ClaimResponse>;
157
+ getLeaderboardVersion(name: string, version: number): Promise<LeaderboardResponse>;
164
158
  /**
165
- * Check if the API is healthy.
166
- * This endpoint does not require an API key.
159
+ * Get a specific version with custom limit.
160
+ */
161
+ getLeaderboardVersion(name: string, version: number, limit: number): Promise<LeaderboardResponse>;
162
+ /**
163
+ * Get a specific version with pagination.
164
+ */
165
+ getLeaderboardVersion(name: string, version: number, limit: number, offset: number): Promise<LeaderboardResponse>;
166
+ /**
167
+ * Get a player's rank and score on the default leaderboard.
168
+ * Returns null if the player has no score.
167
169
  *
168
- * @returns Health status with version and timestamp
170
+ * @example
171
+ * const player = await client.getPlayerRank(playerGuid);
172
+ * if (player) {
173
+ * console.log(`You are ranked #${player.rank}`);
174
+ * }
169
175
  */
170
- healthCheck(): Promise<HealthResponse>;
176
+ getPlayerRank(playerGuid: string): Promise<PlayerResponse | null>;
177
+ /**
178
+ * Get a player's rank and score on a specific leaderboard.
179
+ * Returns null if the player has no score.
180
+ */
181
+ getPlayerRank(playerGuid: string, leaderboard: string): Promise<PlayerResponse | null>;
182
+ /**
183
+ * Update a player's display name on the default leaderboard.
184
+ */
185
+ updatePlayerName(playerGuid: string, newName: string): Promise<PlayerResponse>;
186
+ /**
187
+ * Update a player's display name on a specific leaderboard.
188
+ */
189
+ updatePlayerName(playerGuid: string, newName: string, leaderboard: string): Promise<PlayerResponse>;
171
190
  /**
172
- * Internal request helper with auth and error handling.
191
+ * Claim a migrated score by matching player name.
192
+ * Used when scores were imported without player GUIDs.
173
193
  */
194
+ claimScore(playerGuid: string, playerName: string): Promise<ClaimResponse>;
195
+ /**
196
+ * Claim a migrated score on a specific leaderboard.
197
+ */
198
+ claimScore(playerGuid: string, playerName: string, leaderboard: string): Promise<ClaimResponse>;
199
+ /**
200
+ * Check if the API is healthy.
201
+ * This endpoint does not require an API key.
202
+ */
203
+ healthCheck(): Promise<HealthResponse>;
174
204
  private request;
175
205
  }
176
206
 
@@ -223,4 +253,4 @@ declare class PlayerIdentity {
223
253
  private generateUUID;
224
254
  }
225
255
 
226
- export { type ApiErrorResponse, type ApiResponse, type ApiSuccessResponse, type ClaimRequest, type ClaimResponse, type HealthResponse, KeeperBoardClient, type KeeperBoardConfig, KeeperBoardError, type LeaderboardEntry, type LeaderboardResponse, PlayerIdentity, type PlayerIdentityConfig, type PlayerNameUpdate, type PlayerResponse, type ScoreResponse, type ScoreSubmission };
256
+ export { type ClaimResponse, type HealthResponse, KeeperBoardClient, type KeeperBoardConfig, KeeperBoardError, type LeaderboardEntry, type LeaderboardResponse, PlayerIdentity, type PlayerIdentityConfig, type PlayerResponse, type ResetSchedule, type ScoreResponse, type ScoreSubmission };
package/dist/index.js CHANGED
@@ -37,26 +37,16 @@ var KeeperBoardError = class extends Error {
37
37
  };
38
38
 
39
39
  // src/KeeperBoardClient.ts
40
- var KeeperBoardClient = class {
40
+ var _KeeperBoardClient = class _KeeperBoardClient {
41
41
  constructor(config) {
42
- this.apiUrl = config.apiUrl.replace(/\/$/, "");
42
+ const url = config.apiUrl ?? _KeeperBoardClient.DEFAULT_API_URL;
43
+ this.apiUrl = url.replace(/\/$/, "");
43
44
  this.apiKey = config.apiKey;
44
45
  }
45
- /**
46
- * Submit a score to the leaderboard.
47
- * Only updates if the new score is higher than the existing one.
48
- *
49
- * @param playerGuid - Unique player identifier
50
- * @param playerName - Player display name
51
- * @param score - Score value
52
- * @param metadata - Optional metadata to attach
53
- * @param leaderboardSlug - Optional leaderboard slug (uses default if not specified)
54
- * @returns Score response with rank and whether it's a new high score
55
- */
56
- async submitScore(playerGuid, playerName, score, metadata, leaderboardSlug) {
46
+ async submitScore(playerGuid, playerName, score, leaderboard, metadata) {
57
47
  const params = new URLSearchParams();
58
- if (leaderboardSlug) {
59
- params.set("leaderboard", leaderboardSlug);
48
+ if (leaderboard) {
49
+ params.set("leaderboard", leaderboard);
60
50
  }
61
51
  const url = `${this.apiUrl}/api/v1/scores${params.toString() ? "?" + params.toString() : ""}`;
62
52
  const body = {
@@ -65,49 +55,38 @@ var KeeperBoardClient = class {
65
55
  score,
66
56
  ...metadata && { metadata }
67
57
  };
68
- const response = await this.request(url, {
58
+ return this.request(url, {
69
59
  method: "POST",
70
60
  body: JSON.stringify(body)
71
61
  });
72
- return response;
73
62
  }
74
- /**
75
- * Get the leaderboard entries with pagination.
76
- *
77
- * @param limit - Maximum number of entries to return (default: 10, max: 100)
78
- * @param offset - Pagination offset (default: 0)
79
- * @param leaderboardSlug - Optional leaderboard slug (uses default if not specified)
80
- * @returns Leaderboard entries with total count
81
- */
82
- async getLeaderboard(limit = 10, offset = 0, leaderboardSlug) {
63
+ async getLeaderboard(name, limit = 10, offset = 0) {
83
64
  const params = new URLSearchParams();
84
65
  params.set("limit", String(Math.min(limit, 100)));
85
66
  params.set("offset", String(offset));
86
- if (leaderboardSlug) {
87
- params.set("leaderboard", leaderboardSlug);
67
+ if (name) {
68
+ params.set("leaderboard", name);
88
69
  }
89
70
  const url = `${this.apiUrl}/api/v1/leaderboard?${params.toString()}`;
90
- return this.request(url, {
91
- method: "GET"
92
- });
71
+ return this.request(url, { method: "GET" });
93
72
  }
94
- /**
95
- * Get a specific player's score and rank.
96
- *
97
- * @param playerGuid - Player's unique identifier
98
- * @param leaderboardSlug - Optional leaderboard slug (uses default if not specified)
99
- * @returns Player's score and rank, or null if not found
100
- */
101
- async getPlayer(playerGuid, leaderboardSlug) {
73
+ async getLeaderboardVersion(name, version, limit = 10, offset = 0) {
74
+ const params = new URLSearchParams();
75
+ params.set("leaderboard", name);
76
+ params.set("version", String(version));
77
+ params.set("limit", String(Math.min(limit, 100)));
78
+ params.set("offset", String(offset));
79
+ const url = `${this.apiUrl}/api/v1/leaderboard?${params.toString()}`;
80
+ return this.request(url, { method: "GET" });
81
+ }
82
+ async getPlayerRank(playerGuid, leaderboard) {
102
83
  const params = new URLSearchParams();
103
- if (leaderboardSlug) {
104
- params.set("leaderboard", leaderboardSlug);
84
+ if (leaderboard) {
85
+ params.set("leaderboard", leaderboard);
105
86
  }
106
87
  const url = `${this.apiUrl}/api/v1/player/${encodeURIComponent(playerGuid)}${params.toString() ? "?" + params.toString() : ""}`;
107
88
  try {
108
- return await this.request(url, {
109
- method: "GET"
110
- });
89
+ return await this.request(url, { method: "GET" });
111
90
  } catch (error) {
112
91
  if (error instanceof KeeperBoardError && error.code === "NOT_FOUND") {
113
92
  return null;
@@ -115,18 +94,10 @@ var KeeperBoardClient = class {
115
94
  throw error;
116
95
  }
117
96
  }
118
- /**
119
- * Update a player's display name.
120
- *
121
- * @param playerGuid - Player's unique identifier
122
- * @param newName - New display name
123
- * @param leaderboardSlug - Optional leaderboard slug (uses default if not specified)
124
- * @returns Updated player info
125
- */
126
- async updatePlayerName(playerGuid, newName, leaderboardSlug) {
97
+ async updatePlayerName(playerGuid, newName, leaderboard) {
127
98
  const params = new URLSearchParams();
128
- if (leaderboardSlug) {
129
- params.set("leaderboard", leaderboardSlug);
99
+ if (leaderboard) {
100
+ params.set("leaderboard", leaderboard);
130
101
  }
131
102
  const url = `${this.apiUrl}/api/v1/player/${encodeURIComponent(playerGuid)}${params.toString() ? "?" + params.toString() : ""}`;
132
103
  return this.request(url, {
@@ -134,19 +105,10 @@ var KeeperBoardClient = class {
134
105
  body: JSON.stringify({ player_name: newName })
135
106
  });
136
107
  }
137
- /**
138
- * Claim a migrated score by matching player name.
139
- * Used when scores were imported (e.g., from CSV) without player GUIDs.
140
- *
141
- * @param playerGuid - Player GUID to assign to the claimed score
142
- * @param playerName - Player name to match against migrated scores
143
- * @param leaderboardSlug - Optional leaderboard slug (uses default if not specified)
144
- * @returns Claim result with score and rank
145
- */
146
- async claimScore(playerGuid, playerName, leaderboardSlug) {
108
+ async claimScore(playerGuid, playerName, leaderboard) {
147
109
  const params = new URLSearchParams();
148
- if (leaderboardSlug) {
149
- params.set("leaderboard", leaderboardSlug);
110
+ if (leaderboard) {
111
+ params.set("leaderboard", leaderboard);
150
112
  }
151
113
  const url = `${this.apiUrl}/api/v1/claim${params.toString() ? "?" + params.toString() : ""}`;
152
114
  return this.request(url, {
@@ -157,19 +119,18 @@ var KeeperBoardClient = class {
157
119
  })
158
120
  });
159
121
  }
122
+ // ============================================
123
+ // HEALTH CHECK
124
+ // ============================================
160
125
  /**
161
126
  * Check if the API is healthy.
162
127
  * This endpoint does not require an API key.
163
- *
164
- * @returns Health status with version and timestamp
165
128
  */
166
129
  async healthCheck() {
167
130
  const url = `${this.apiUrl}/api/v1/health`;
168
131
  const response = await fetch(url, {
169
132
  method: "GET",
170
- headers: {
171
- Accept: "application/json"
172
- }
133
+ headers: { Accept: "application/json" }
173
134
  });
174
135
  const json = await response.json();
175
136
  if (!json.success) {
@@ -177,9 +138,9 @@ var KeeperBoardClient = class {
177
138
  }
178
139
  return json.data;
179
140
  }
180
- /**
181
- * Internal request helper with auth and error handling.
182
- */
141
+ // ============================================
142
+ // INTERNAL
143
+ // ============================================
183
144
  async request(url, options) {
184
145
  const headers = {
185
146
  "Content-Type": "application/json",
@@ -188,10 +149,7 @@ var KeeperBoardClient = class {
188
149
  };
189
150
  const response = await fetch(url, {
190
151
  ...options,
191
- headers: {
192
- ...headers,
193
- ...options.headers || {}
194
- }
152
+ headers: { ...headers, ...options.headers || {} }
195
153
  });
196
154
  const json = await response.json();
197
155
  if (!json.success) {
@@ -200,6 +158,8 @@ var KeeperBoardClient = class {
200
158
  return json.data;
201
159
  }
202
160
  };
161
+ _KeeperBoardClient.DEFAULT_API_URL = "https://keeperboard.vercel.app";
162
+ var KeeperBoardClient = _KeeperBoardClient;
203
163
 
204
164
  // src/PlayerIdentity.ts
205
165
  var DEFAULT_KEY_PREFIX = "keeperboard_";
package/dist/index.mjs CHANGED
@@ -9,26 +9,16 @@ var KeeperBoardError = class extends Error {
9
9
  };
10
10
 
11
11
  // src/KeeperBoardClient.ts
12
- var KeeperBoardClient = class {
12
+ var _KeeperBoardClient = class _KeeperBoardClient {
13
13
  constructor(config) {
14
- this.apiUrl = config.apiUrl.replace(/\/$/, "");
14
+ const url = config.apiUrl ?? _KeeperBoardClient.DEFAULT_API_URL;
15
+ this.apiUrl = url.replace(/\/$/, "");
15
16
  this.apiKey = config.apiKey;
16
17
  }
17
- /**
18
- * Submit a score to the leaderboard.
19
- * Only updates if the new score is higher than the existing one.
20
- *
21
- * @param playerGuid - Unique player identifier
22
- * @param playerName - Player display name
23
- * @param score - Score value
24
- * @param metadata - Optional metadata to attach
25
- * @param leaderboardSlug - Optional leaderboard slug (uses default if not specified)
26
- * @returns Score response with rank and whether it's a new high score
27
- */
28
- async submitScore(playerGuid, playerName, score, metadata, leaderboardSlug) {
18
+ async submitScore(playerGuid, playerName, score, leaderboard, metadata) {
29
19
  const params = new URLSearchParams();
30
- if (leaderboardSlug) {
31
- params.set("leaderboard", leaderboardSlug);
20
+ if (leaderboard) {
21
+ params.set("leaderboard", leaderboard);
32
22
  }
33
23
  const url = `${this.apiUrl}/api/v1/scores${params.toString() ? "?" + params.toString() : ""}`;
34
24
  const body = {
@@ -37,49 +27,38 @@ var KeeperBoardClient = class {
37
27
  score,
38
28
  ...metadata && { metadata }
39
29
  };
40
- const response = await this.request(url, {
30
+ return this.request(url, {
41
31
  method: "POST",
42
32
  body: JSON.stringify(body)
43
33
  });
44
- return response;
45
34
  }
46
- /**
47
- * Get the leaderboard entries with pagination.
48
- *
49
- * @param limit - Maximum number of entries to return (default: 10, max: 100)
50
- * @param offset - Pagination offset (default: 0)
51
- * @param leaderboardSlug - Optional leaderboard slug (uses default if not specified)
52
- * @returns Leaderboard entries with total count
53
- */
54
- async getLeaderboard(limit = 10, offset = 0, leaderboardSlug) {
35
+ async getLeaderboard(name, limit = 10, offset = 0) {
55
36
  const params = new URLSearchParams();
56
37
  params.set("limit", String(Math.min(limit, 100)));
57
38
  params.set("offset", String(offset));
58
- if (leaderboardSlug) {
59
- params.set("leaderboard", leaderboardSlug);
39
+ if (name) {
40
+ params.set("leaderboard", name);
60
41
  }
61
42
  const url = `${this.apiUrl}/api/v1/leaderboard?${params.toString()}`;
62
- return this.request(url, {
63
- method: "GET"
64
- });
43
+ return this.request(url, { method: "GET" });
65
44
  }
66
- /**
67
- * Get a specific player's score and rank.
68
- *
69
- * @param playerGuid - Player's unique identifier
70
- * @param leaderboardSlug - Optional leaderboard slug (uses default if not specified)
71
- * @returns Player's score and rank, or null if not found
72
- */
73
- async getPlayer(playerGuid, leaderboardSlug) {
45
+ async getLeaderboardVersion(name, version, limit = 10, offset = 0) {
46
+ const params = new URLSearchParams();
47
+ params.set("leaderboard", name);
48
+ params.set("version", String(version));
49
+ params.set("limit", String(Math.min(limit, 100)));
50
+ params.set("offset", String(offset));
51
+ const url = `${this.apiUrl}/api/v1/leaderboard?${params.toString()}`;
52
+ return this.request(url, { method: "GET" });
53
+ }
54
+ async getPlayerRank(playerGuid, leaderboard) {
74
55
  const params = new URLSearchParams();
75
- if (leaderboardSlug) {
76
- params.set("leaderboard", leaderboardSlug);
56
+ if (leaderboard) {
57
+ params.set("leaderboard", leaderboard);
77
58
  }
78
59
  const url = `${this.apiUrl}/api/v1/player/${encodeURIComponent(playerGuid)}${params.toString() ? "?" + params.toString() : ""}`;
79
60
  try {
80
- return await this.request(url, {
81
- method: "GET"
82
- });
61
+ return await this.request(url, { method: "GET" });
83
62
  } catch (error) {
84
63
  if (error instanceof KeeperBoardError && error.code === "NOT_FOUND") {
85
64
  return null;
@@ -87,18 +66,10 @@ var KeeperBoardClient = class {
87
66
  throw error;
88
67
  }
89
68
  }
90
- /**
91
- * Update a player's display name.
92
- *
93
- * @param playerGuid - Player's unique identifier
94
- * @param newName - New display name
95
- * @param leaderboardSlug - Optional leaderboard slug (uses default if not specified)
96
- * @returns Updated player info
97
- */
98
- async updatePlayerName(playerGuid, newName, leaderboardSlug) {
69
+ async updatePlayerName(playerGuid, newName, leaderboard) {
99
70
  const params = new URLSearchParams();
100
- if (leaderboardSlug) {
101
- params.set("leaderboard", leaderboardSlug);
71
+ if (leaderboard) {
72
+ params.set("leaderboard", leaderboard);
102
73
  }
103
74
  const url = `${this.apiUrl}/api/v1/player/${encodeURIComponent(playerGuid)}${params.toString() ? "?" + params.toString() : ""}`;
104
75
  return this.request(url, {
@@ -106,19 +77,10 @@ var KeeperBoardClient = class {
106
77
  body: JSON.stringify({ player_name: newName })
107
78
  });
108
79
  }
109
- /**
110
- * Claim a migrated score by matching player name.
111
- * Used when scores were imported (e.g., from CSV) without player GUIDs.
112
- *
113
- * @param playerGuid - Player GUID to assign to the claimed score
114
- * @param playerName - Player name to match against migrated scores
115
- * @param leaderboardSlug - Optional leaderboard slug (uses default if not specified)
116
- * @returns Claim result with score and rank
117
- */
118
- async claimScore(playerGuid, playerName, leaderboardSlug) {
80
+ async claimScore(playerGuid, playerName, leaderboard) {
119
81
  const params = new URLSearchParams();
120
- if (leaderboardSlug) {
121
- params.set("leaderboard", leaderboardSlug);
82
+ if (leaderboard) {
83
+ params.set("leaderboard", leaderboard);
122
84
  }
123
85
  const url = `${this.apiUrl}/api/v1/claim${params.toString() ? "?" + params.toString() : ""}`;
124
86
  return this.request(url, {
@@ -129,19 +91,18 @@ var KeeperBoardClient = class {
129
91
  })
130
92
  });
131
93
  }
94
+ // ============================================
95
+ // HEALTH CHECK
96
+ // ============================================
132
97
  /**
133
98
  * Check if the API is healthy.
134
99
  * This endpoint does not require an API key.
135
- *
136
- * @returns Health status with version and timestamp
137
100
  */
138
101
  async healthCheck() {
139
102
  const url = `${this.apiUrl}/api/v1/health`;
140
103
  const response = await fetch(url, {
141
104
  method: "GET",
142
- headers: {
143
- Accept: "application/json"
144
- }
105
+ headers: { Accept: "application/json" }
145
106
  });
146
107
  const json = await response.json();
147
108
  if (!json.success) {
@@ -149,9 +110,9 @@ var KeeperBoardClient = class {
149
110
  }
150
111
  return json.data;
151
112
  }
152
- /**
153
- * Internal request helper with auth and error handling.
154
- */
113
+ // ============================================
114
+ // INTERNAL
115
+ // ============================================
155
116
  async request(url, options) {
156
117
  const headers = {
157
118
  "Content-Type": "application/json",
@@ -160,10 +121,7 @@ var KeeperBoardClient = class {
160
121
  };
161
122
  const response = await fetch(url, {
162
123
  ...options,
163
- headers: {
164
- ...headers,
165
- ...options.headers || {}
166
- }
124
+ headers: { ...headers, ...options.headers || {} }
167
125
  });
168
126
  const json = await response.json();
169
127
  if (!json.success) {
@@ -172,6 +130,8 @@ var KeeperBoardClient = class {
172
130
  return json.data;
173
131
  }
174
132
  };
133
+ _KeeperBoardClient.DEFAULT_API_URL = "https://keeperboard.vercel.app";
134
+ var KeeperBoardClient = _KeeperBoardClient;
175
135
 
176
136
  // src/PlayerIdentity.ts
177
137
  var DEFAULT_KEY_PREFIX = "keeperboard_";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keeperboard",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "TypeScript client SDK for KeeperBoard leaderboard-as-a-service",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -19,6 +19,8 @@
19
19
  "build": "tsup src/index.ts --format cjs,esm --dts",
20
20
  "typecheck": "tsc --noEmit",
21
21
  "clean": "rm -rf dist",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest",
22
24
  "prepublishOnly": "npm run build"
23
25
  },
24
26
  "keywords": [
@@ -42,8 +44,11 @@
42
44
  "url": "https://github.com/clauderoy790/keeperboard/issues"
43
45
  },
44
46
  "devDependencies": {
47
+ "@supabase/supabase-js": "^2.95.3",
48
+ "dotenv": "^17.2.4",
45
49
  "tsup": "^8.0.0",
46
- "typescript": "^5.0.0"
50
+ "typescript": "^5.0.0",
51
+ "vitest": "^4.0.18"
47
52
  },
48
53
  "engines": {
49
54
  "node": ">=18"