keeperboard-sdk 1.0.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 +345 -0
- package/dist/index.d.mts +226 -0
- package/dist/index.d.ts +226 -0
- package/dist/index.js +296 -0
- package/dist/index.mjs +267 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# KeeperBoard SDK
|
|
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.
|
|
4
|
+
|
|
5
|
+
Works with Phaser.js, vanilla JavaScript, and any TypeScript/JavaScript game running in the browser.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install keeperboard-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Alternative: Copy source directly
|
|
14
|
+
|
|
15
|
+
If you prefer not to use npm, copy the `src/` folder into your project:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { KeeperBoardClient, PlayerIdentity } from './keeperboard/index';
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### 1. Get your API key
|
|
24
|
+
|
|
25
|
+
1. Sign up at your KeeperBoard dashboard
|
|
26
|
+
2. Create a game
|
|
27
|
+
3. Generate an API key for your environment (dev/prod)
|
|
28
|
+
|
|
29
|
+
### 2. Initialize the client
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { KeeperBoardClient, PlayerIdentity } from 'keeperboard-sdk';
|
|
33
|
+
|
|
34
|
+
// Create the API client
|
|
35
|
+
const keeperboard = new KeeperBoardClient({
|
|
36
|
+
apiUrl: 'https://keeperboard.vercel.app',
|
|
37
|
+
apiKey: 'kb_dev_your_api_key_here',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Helper for persistent player identity
|
|
41
|
+
const playerIdentity = new PlayerIdentity();
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 3. Submit a score
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// Get or create a persistent player GUID
|
|
48
|
+
const playerGuid = playerIdentity.getOrCreatePlayerGuid();
|
|
49
|
+
|
|
50
|
+
// Submit a score (only updates if higher than existing)
|
|
51
|
+
const result = await keeperboard.submitScore(playerGuid, 'PlayerName', 1500);
|
|
52
|
+
|
|
53
|
+
console.log(`Rank: #${result.rank}`);
|
|
54
|
+
console.log(`New high score: ${result.is_new_high_score}`);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 4. Display the leaderboard
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
const leaderboard = await keeperboard.getLeaderboard(10);
|
|
61
|
+
|
|
62
|
+
leaderboard.entries.forEach((entry) => {
|
|
63
|
+
console.log(`#${entry.rank} ${entry.player_name}: ${entry.score}`);
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## API Reference
|
|
68
|
+
|
|
69
|
+
### KeeperBoardClient
|
|
70
|
+
|
|
71
|
+
#### Constructor
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
const client = new KeeperBoardClient({
|
|
75
|
+
apiUrl: string, // Your KeeperBoard API URL
|
|
76
|
+
apiKey: string, // API key from dashboard (e.g., "kb_dev_...")
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
#### Methods
|
|
81
|
+
|
|
82
|
+
##### `submitScore(playerGuid, playerName, score, metadata?, leaderboardSlug?)`
|
|
83
|
+
|
|
84
|
+
Submit a score. Only updates if the new score is higher than the existing one.
|
|
85
|
+
|
|
86
|
+
```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
|
+
// }
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
##### `getLeaderboard(limit?, offset?, leaderboardSlug?)`
|
|
107
|
+
|
|
108
|
+
Get leaderboard entries with pagination.
|
|
109
|
+
|
|
110
|
+
```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
|
+
// }
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
##### `getPlayer(playerGuid, leaderboardSlug?)`
|
|
125
|
+
|
|
126
|
+
Get a player's score and rank. Returns `null` if not found.
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
const player = await client.getPlayer('player-uuid-123');
|
|
130
|
+
|
|
131
|
+
if (player) {
|
|
132
|
+
console.log(`Score: ${player.score}, Rank: #${player.rank}`);
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
##### `updatePlayerName(playerGuid, newName, leaderboardSlug?)`
|
|
137
|
+
|
|
138
|
+
Update a player's display name.
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
const result = await client.updatePlayerName(
|
|
142
|
+
'player-uuid-123',
|
|
143
|
+
'NewPlayerName',
|
|
144
|
+
);
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
##### `claimScore(playerGuid, playerName, leaderboardSlug?)`
|
|
148
|
+
|
|
149
|
+
Claim a migrated score by matching player name. Used when scores were imported without player GUIDs.
|
|
150
|
+
|
|
151
|
+
```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
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
##### `healthCheck()`
|
|
166
|
+
|
|
167
|
+
Check if the API is healthy. Does not require an API key.
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
const health = await client.healthCheck();
|
|
171
|
+
console.log(`API Version: ${health.version}`);
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### PlayerIdentity
|
|
175
|
+
|
|
176
|
+
Helper for managing persistent player identity in localStorage.
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const identity = new PlayerIdentity({
|
|
180
|
+
keyPrefix: 'mygame_', // optional, default: 'keeperboard_'
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Get or create a persistent player GUID
|
|
184
|
+
const guid = identity.getOrCreatePlayerGuid();
|
|
185
|
+
|
|
186
|
+
// Store/retrieve player name
|
|
187
|
+
identity.setPlayerName('PlayerName');
|
|
188
|
+
const name = identity.getPlayerName();
|
|
189
|
+
|
|
190
|
+
// Check if identity exists
|
|
191
|
+
if (identity.hasIdentity()) {
|
|
192
|
+
// returning player
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Clear identity (e.g., for "Sign Out")
|
|
196
|
+
identity.clear();
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Error Handling
|
|
200
|
+
|
|
201
|
+
All methods throw `KeeperBoardError` on failure:
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
import { KeeperBoardError } from 'keeperboard-sdk';
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
await client.submitScore(playerGuid, name, score);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (error instanceof KeeperBoardError) {
|
|
210
|
+
console.error(`Error [${error.code}]: ${error.message}`);
|
|
211
|
+
|
|
212
|
+
switch (error.code) {
|
|
213
|
+
case 'INVALID_API_KEY':
|
|
214
|
+
// Check your API key
|
|
215
|
+
break;
|
|
216
|
+
case 'NOT_FOUND':
|
|
217
|
+
// Player or leaderboard not found
|
|
218
|
+
break;
|
|
219
|
+
case 'INVALID_REQUEST':
|
|
220
|
+
// Check request parameters
|
|
221
|
+
break;
|
|
222
|
+
case 'ALREADY_CLAIMED':
|
|
223
|
+
// Player already has a score
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Phaser.js Integration Example
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
import { KeeperBoardClient, PlayerIdentity } from 'keeperboard-sdk';
|
|
234
|
+
|
|
235
|
+
const keeperboard = new KeeperBoardClient({
|
|
236
|
+
apiUrl: 'https://keeperboard.vercel.app',
|
|
237
|
+
apiKey: 'kb_prod_your_api_key',
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const playerIdentity = new PlayerIdentity();
|
|
241
|
+
|
|
242
|
+
class GameOverScene extends Phaser.Scene {
|
|
243
|
+
private score: number = 0;
|
|
244
|
+
|
|
245
|
+
init(data: { score: number }) {
|
|
246
|
+
this.score = data.score;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async create() {
|
|
250
|
+
const playerGuid = playerIdentity.getOrCreatePlayerGuid();
|
|
251
|
+
const playerName = playerIdentity.getPlayerName() ?? 'Anonymous';
|
|
252
|
+
|
|
253
|
+
// Submit score
|
|
254
|
+
const result = await keeperboard.submitScore(
|
|
255
|
+
playerGuid,
|
|
256
|
+
playerName,
|
|
257
|
+
this.score,
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// Display result
|
|
261
|
+
this.add
|
|
262
|
+
.text(400, 200, `Your Rank: #${result.rank}`, {
|
|
263
|
+
fontSize: '32px',
|
|
264
|
+
})
|
|
265
|
+
.setOrigin(0.5);
|
|
266
|
+
|
|
267
|
+
if (result.is_new_high_score) {
|
|
268
|
+
this.add
|
|
269
|
+
.text(400, 250, 'NEW HIGH SCORE!', {
|
|
270
|
+
fontSize: '24px',
|
|
271
|
+
color: '#ffff00',
|
|
272
|
+
})
|
|
273
|
+
.setOrigin(0.5);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Display leaderboard
|
|
277
|
+
const leaderboard = await keeperboard.getLeaderboard(10);
|
|
278
|
+
|
|
279
|
+
leaderboard.entries.forEach((entry, index) => {
|
|
280
|
+
const isMe = entry.player_guid === playerGuid;
|
|
281
|
+
const color = isMe ? '#00ff00' : '#ffffff';
|
|
282
|
+
|
|
283
|
+
this.add
|
|
284
|
+
.text(
|
|
285
|
+
400,
|
|
286
|
+
350 + index * 30,
|
|
287
|
+
`#${entry.rank} ${entry.player_name}: ${entry.score}`,
|
|
288
|
+
{ fontSize: '18px', color },
|
|
289
|
+
)
|
|
290
|
+
.setOrigin(0.5);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
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
|
+
```
|
|
326
|
+
|
|
327
|
+
## Development
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
# Install dependencies
|
|
331
|
+
npm install
|
|
332
|
+
|
|
333
|
+
# Type check
|
|
334
|
+
npm run typecheck
|
|
335
|
+
|
|
336
|
+
# Build
|
|
337
|
+
npm run build
|
|
338
|
+
|
|
339
|
+
# Clean
|
|
340
|
+
npm run clean
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## License
|
|
344
|
+
|
|
345
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for KeeperBoard SDK.
|
|
3
|
+
* Matches the API response shapes from the KeeperBoard public API.
|
|
4
|
+
*/
|
|
5
|
+
interface KeeperBoardConfig {
|
|
6
|
+
/** Base URL of the KeeperBoard API (e.g., "https://keeperboard.vercel.app") */
|
|
7
|
+
apiUrl: string;
|
|
8
|
+
/** API key from the KeeperBoard dashboard (e.g., "kb_dev_abc123...") */
|
|
9
|
+
apiKey: string;
|
|
10
|
+
}
|
|
11
|
+
interface ScoreSubmission {
|
|
12
|
+
/** Unique player identifier (UUID or custom string) */
|
|
13
|
+
player_guid: string;
|
|
14
|
+
/** Player display name */
|
|
15
|
+
player_name: string;
|
|
16
|
+
/** Score value */
|
|
17
|
+
score: number;
|
|
18
|
+
/** Optional metadata to attach to the score */
|
|
19
|
+
metadata?: Record<string, unknown>;
|
|
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
|
+
}
|
|
31
|
+
interface ScoreResponse {
|
|
32
|
+
/** Score ID in the database */
|
|
33
|
+
id: string;
|
|
34
|
+
/** Player's unique identifier */
|
|
35
|
+
player_guid: string;
|
|
36
|
+
/** Player's display name */
|
|
37
|
+
player_name: string;
|
|
38
|
+
/** Current score value */
|
|
39
|
+
score: number;
|
|
40
|
+
/** Player's rank on the leaderboard */
|
|
41
|
+
rank: number;
|
|
42
|
+
/** Whether this submission resulted in a new high score */
|
|
43
|
+
is_new_high_score: boolean;
|
|
44
|
+
}
|
|
45
|
+
interface LeaderboardEntry {
|
|
46
|
+
/** Position on the leaderboard (1-indexed) */
|
|
47
|
+
rank: number;
|
|
48
|
+
/** Player's unique identifier */
|
|
49
|
+
player_guid: string;
|
|
50
|
+
/** Player's display name */
|
|
51
|
+
player_name: string;
|
|
52
|
+
/** Score value */
|
|
53
|
+
score: number;
|
|
54
|
+
}
|
|
55
|
+
interface LeaderboardResponse {
|
|
56
|
+
/** Array of leaderboard entries */
|
|
57
|
+
entries: LeaderboardEntry[];
|
|
58
|
+
/** Total number of scores on this leaderboard */
|
|
59
|
+
total_count: number;
|
|
60
|
+
}
|
|
61
|
+
interface PlayerResponse {
|
|
62
|
+
/** Score ID in the database */
|
|
63
|
+
id: string;
|
|
64
|
+
/** Player's unique identifier */
|
|
65
|
+
player_guid: string;
|
|
66
|
+
/** Player's display name */
|
|
67
|
+
player_name: string;
|
|
68
|
+
/** Player's score */
|
|
69
|
+
score: number;
|
|
70
|
+
/** Player's rank on the leaderboard */
|
|
71
|
+
rank: number;
|
|
72
|
+
}
|
|
73
|
+
interface ClaimResponse {
|
|
74
|
+
/** Whether the score was successfully claimed */
|
|
75
|
+
claimed: boolean;
|
|
76
|
+
/** The claimed score value */
|
|
77
|
+
score: number;
|
|
78
|
+
/** Player's rank after claiming */
|
|
79
|
+
rank: number;
|
|
80
|
+
/** The player name that was matched */
|
|
81
|
+
player_name: string;
|
|
82
|
+
}
|
|
83
|
+
interface HealthResponse {
|
|
84
|
+
/** Service name */
|
|
85
|
+
service: string;
|
|
86
|
+
/** API version */
|
|
87
|
+
version: string;
|
|
88
|
+
/** Server timestamp */
|
|
89
|
+
timestamp: string;
|
|
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
|
+
declare class KeeperBoardError extends Error {
|
|
102
|
+
readonly code: string;
|
|
103
|
+
readonly statusCode: number;
|
|
104
|
+
constructor(message: string, code: string, statusCode: number);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* KeeperBoard API client for TypeScript/JavaScript games.
|
|
109
|
+
* Works in any browser environment using the fetch API.
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
declare class KeeperBoardClient {
|
|
113
|
+
private readonly apiUrl;
|
|
114
|
+
private readonly apiKey;
|
|
115
|
+
constructor(config: KeeperBoardConfig);
|
|
116
|
+
/**
|
|
117
|
+
* Submit a score to the leaderboard.
|
|
118
|
+
* Only updates if the new score is higher than the existing one.
|
|
119
|
+
*
|
|
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
|
|
126
|
+
*/
|
|
127
|
+
submitScore(playerGuid: string, playerName: string, score: number, metadata?: Record<string, unknown>, leaderboardSlug?: string): Promise<ScoreResponse>;
|
|
128
|
+
/**
|
|
129
|
+
* Get the leaderboard entries with pagination.
|
|
130
|
+
*
|
|
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
|
|
135
|
+
*/
|
|
136
|
+
getLeaderboard(limit?: number, offset?: number, leaderboardSlug?: string): Promise<LeaderboardResponse>;
|
|
137
|
+
/**
|
|
138
|
+
* Get a specific player's score and rank.
|
|
139
|
+
*
|
|
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
|
|
143
|
+
*/
|
|
144
|
+
getPlayer(playerGuid: string, leaderboardSlug?: string): Promise<PlayerResponse | null>;
|
|
145
|
+
/**
|
|
146
|
+
* Update a player's display name.
|
|
147
|
+
*
|
|
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
|
|
152
|
+
*/
|
|
153
|
+
updatePlayerName(playerGuid: string, newName: string, leaderboardSlug?: string): Promise<PlayerResponse>;
|
|
154
|
+
/**
|
|
155
|
+
* Claim a migrated score by matching player name.
|
|
156
|
+
* Used when scores were imported (e.g., from CSV) without player GUIDs.
|
|
157
|
+
*
|
|
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
|
|
162
|
+
*/
|
|
163
|
+
claimScore(playerGuid: string, playerName: string, leaderboardSlug?: string): Promise<ClaimResponse>;
|
|
164
|
+
/**
|
|
165
|
+
* Check if the API is healthy.
|
|
166
|
+
* This endpoint does not require an API key.
|
|
167
|
+
*
|
|
168
|
+
* @returns Health status with version and timestamp
|
|
169
|
+
*/
|
|
170
|
+
healthCheck(): Promise<HealthResponse>;
|
|
171
|
+
/**
|
|
172
|
+
* Internal request helper with auth and error handling.
|
|
173
|
+
*/
|
|
174
|
+
private request;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Helper class for managing player identity in localStorage.
|
|
179
|
+
* Provides persistent player GUID and name storage across game sessions.
|
|
180
|
+
*/
|
|
181
|
+
interface PlayerIdentityConfig {
|
|
182
|
+
/** Prefix for localStorage keys (default: "keeperboard_") */
|
|
183
|
+
keyPrefix?: string;
|
|
184
|
+
}
|
|
185
|
+
declare class PlayerIdentity {
|
|
186
|
+
private readonly keyPrefix;
|
|
187
|
+
private readonly guidKey;
|
|
188
|
+
private readonly nameKey;
|
|
189
|
+
constructor(config?: PlayerIdentityConfig);
|
|
190
|
+
/**
|
|
191
|
+
* Get the stored player GUID, or null if none exists.
|
|
192
|
+
*/
|
|
193
|
+
getPlayerGuid(): string | null;
|
|
194
|
+
/**
|
|
195
|
+
* Set the player GUID in localStorage.
|
|
196
|
+
*/
|
|
197
|
+
setPlayerGuid(guid: string): void;
|
|
198
|
+
/**
|
|
199
|
+
* Get the stored player GUID, creating one if it doesn't exist.
|
|
200
|
+
* Uses crypto.randomUUID() for generating new GUIDs.
|
|
201
|
+
*/
|
|
202
|
+
getOrCreatePlayerGuid(): string;
|
|
203
|
+
/**
|
|
204
|
+
* Get the stored player name, or null if none exists.
|
|
205
|
+
*/
|
|
206
|
+
getPlayerName(): string | null;
|
|
207
|
+
/**
|
|
208
|
+
* Set the player name in localStorage.
|
|
209
|
+
*/
|
|
210
|
+
setPlayerName(name: string): void;
|
|
211
|
+
/**
|
|
212
|
+
* Clear all stored player identity data.
|
|
213
|
+
*/
|
|
214
|
+
clear(): void;
|
|
215
|
+
/**
|
|
216
|
+
* Check if player identity is stored.
|
|
217
|
+
*/
|
|
218
|
+
hasIdentity(): boolean;
|
|
219
|
+
/**
|
|
220
|
+
* Generate a UUID v4.
|
|
221
|
+
* Uses crypto.randomUUID() if available, otherwise falls back to a manual implementation.
|
|
222
|
+
*/
|
|
223
|
+
private generateUUID;
|
|
224
|
+
}
|
|
225
|
+
|
|
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 };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for KeeperBoard SDK.
|
|
3
|
+
* Matches the API response shapes from the KeeperBoard public API.
|
|
4
|
+
*/
|
|
5
|
+
interface KeeperBoardConfig {
|
|
6
|
+
/** Base URL of the KeeperBoard API (e.g., "https://keeperboard.vercel.app") */
|
|
7
|
+
apiUrl: string;
|
|
8
|
+
/** API key from the KeeperBoard dashboard (e.g., "kb_dev_abc123...") */
|
|
9
|
+
apiKey: string;
|
|
10
|
+
}
|
|
11
|
+
interface ScoreSubmission {
|
|
12
|
+
/** Unique player identifier (UUID or custom string) */
|
|
13
|
+
player_guid: string;
|
|
14
|
+
/** Player display name */
|
|
15
|
+
player_name: string;
|
|
16
|
+
/** Score value */
|
|
17
|
+
score: number;
|
|
18
|
+
/** Optional metadata to attach to the score */
|
|
19
|
+
metadata?: Record<string, unknown>;
|
|
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
|
+
}
|
|
31
|
+
interface ScoreResponse {
|
|
32
|
+
/** Score ID in the database */
|
|
33
|
+
id: string;
|
|
34
|
+
/** Player's unique identifier */
|
|
35
|
+
player_guid: string;
|
|
36
|
+
/** Player's display name */
|
|
37
|
+
player_name: string;
|
|
38
|
+
/** Current score value */
|
|
39
|
+
score: number;
|
|
40
|
+
/** Player's rank on the leaderboard */
|
|
41
|
+
rank: number;
|
|
42
|
+
/** Whether this submission resulted in a new high score */
|
|
43
|
+
is_new_high_score: boolean;
|
|
44
|
+
}
|
|
45
|
+
interface LeaderboardEntry {
|
|
46
|
+
/** Position on the leaderboard (1-indexed) */
|
|
47
|
+
rank: number;
|
|
48
|
+
/** Player's unique identifier */
|
|
49
|
+
player_guid: string;
|
|
50
|
+
/** Player's display name */
|
|
51
|
+
player_name: string;
|
|
52
|
+
/** Score value */
|
|
53
|
+
score: number;
|
|
54
|
+
}
|
|
55
|
+
interface LeaderboardResponse {
|
|
56
|
+
/** Array of leaderboard entries */
|
|
57
|
+
entries: LeaderboardEntry[];
|
|
58
|
+
/** Total number of scores on this leaderboard */
|
|
59
|
+
total_count: number;
|
|
60
|
+
}
|
|
61
|
+
interface PlayerResponse {
|
|
62
|
+
/** Score ID in the database */
|
|
63
|
+
id: string;
|
|
64
|
+
/** Player's unique identifier */
|
|
65
|
+
player_guid: string;
|
|
66
|
+
/** Player's display name */
|
|
67
|
+
player_name: string;
|
|
68
|
+
/** Player's score */
|
|
69
|
+
score: number;
|
|
70
|
+
/** Player's rank on the leaderboard */
|
|
71
|
+
rank: number;
|
|
72
|
+
}
|
|
73
|
+
interface ClaimResponse {
|
|
74
|
+
/** Whether the score was successfully claimed */
|
|
75
|
+
claimed: boolean;
|
|
76
|
+
/** The claimed score value */
|
|
77
|
+
score: number;
|
|
78
|
+
/** Player's rank after claiming */
|
|
79
|
+
rank: number;
|
|
80
|
+
/** The player name that was matched */
|
|
81
|
+
player_name: string;
|
|
82
|
+
}
|
|
83
|
+
interface HealthResponse {
|
|
84
|
+
/** Service name */
|
|
85
|
+
service: string;
|
|
86
|
+
/** API version */
|
|
87
|
+
version: string;
|
|
88
|
+
/** Server timestamp */
|
|
89
|
+
timestamp: string;
|
|
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
|
+
declare class KeeperBoardError extends Error {
|
|
102
|
+
readonly code: string;
|
|
103
|
+
readonly statusCode: number;
|
|
104
|
+
constructor(message: string, code: string, statusCode: number);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* KeeperBoard API client for TypeScript/JavaScript games.
|
|
109
|
+
* Works in any browser environment using the fetch API.
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
declare class KeeperBoardClient {
|
|
113
|
+
private readonly apiUrl;
|
|
114
|
+
private readonly apiKey;
|
|
115
|
+
constructor(config: KeeperBoardConfig);
|
|
116
|
+
/**
|
|
117
|
+
* Submit a score to the leaderboard.
|
|
118
|
+
* Only updates if the new score is higher than the existing one.
|
|
119
|
+
*
|
|
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
|
|
126
|
+
*/
|
|
127
|
+
submitScore(playerGuid: string, playerName: string, score: number, metadata?: Record<string, unknown>, leaderboardSlug?: string): Promise<ScoreResponse>;
|
|
128
|
+
/**
|
|
129
|
+
* Get the leaderboard entries with pagination.
|
|
130
|
+
*
|
|
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
|
|
135
|
+
*/
|
|
136
|
+
getLeaderboard(limit?: number, offset?: number, leaderboardSlug?: string): Promise<LeaderboardResponse>;
|
|
137
|
+
/**
|
|
138
|
+
* Get a specific player's score and rank.
|
|
139
|
+
*
|
|
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
|
|
143
|
+
*/
|
|
144
|
+
getPlayer(playerGuid: string, leaderboardSlug?: string): Promise<PlayerResponse | null>;
|
|
145
|
+
/**
|
|
146
|
+
* Update a player's display name.
|
|
147
|
+
*
|
|
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
|
|
152
|
+
*/
|
|
153
|
+
updatePlayerName(playerGuid: string, newName: string, leaderboardSlug?: string): Promise<PlayerResponse>;
|
|
154
|
+
/**
|
|
155
|
+
* Claim a migrated score by matching player name.
|
|
156
|
+
* Used when scores were imported (e.g., from CSV) without player GUIDs.
|
|
157
|
+
*
|
|
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
|
|
162
|
+
*/
|
|
163
|
+
claimScore(playerGuid: string, playerName: string, leaderboardSlug?: string): Promise<ClaimResponse>;
|
|
164
|
+
/**
|
|
165
|
+
* Check if the API is healthy.
|
|
166
|
+
* This endpoint does not require an API key.
|
|
167
|
+
*
|
|
168
|
+
* @returns Health status with version and timestamp
|
|
169
|
+
*/
|
|
170
|
+
healthCheck(): Promise<HealthResponse>;
|
|
171
|
+
/**
|
|
172
|
+
* Internal request helper with auth and error handling.
|
|
173
|
+
*/
|
|
174
|
+
private request;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Helper class for managing player identity in localStorage.
|
|
179
|
+
* Provides persistent player GUID and name storage across game sessions.
|
|
180
|
+
*/
|
|
181
|
+
interface PlayerIdentityConfig {
|
|
182
|
+
/** Prefix for localStorage keys (default: "keeperboard_") */
|
|
183
|
+
keyPrefix?: string;
|
|
184
|
+
}
|
|
185
|
+
declare class PlayerIdentity {
|
|
186
|
+
private readonly keyPrefix;
|
|
187
|
+
private readonly guidKey;
|
|
188
|
+
private readonly nameKey;
|
|
189
|
+
constructor(config?: PlayerIdentityConfig);
|
|
190
|
+
/**
|
|
191
|
+
* Get the stored player GUID, or null if none exists.
|
|
192
|
+
*/
|
|
193
|
+
getPlayerGuid(): string | null;
|
|
194
|
+
/**
|
|
195
|
+
* Set the player GUID in localStorage.
|
|
196
|
+
*/
|
|
197
|
+
setPlayerGuid(guid: string): void;
|
|
198
|
+
/**
|
|
199
|
+
* Get the stored player GUID, creating one if it doesn't exist.
|
|
200
|
+
* Uses crypto.randomUUID() for generating new GUIDs.
|
|
201
|
+
*/
|
|
202
|
+
getOrCreatePlayerGuid(): string;
|
|
203
|
+
/**
|
|
204
|
+
* Get the stored player name, or null if none exists.
|
|
205
|
+
*/
|
|
206
|
+
getPlayerName(): string | null;
|
|
207
|
+
/**
|
|
208
|
+
* Set the player name in localStorage.
|
|
209
|
+
*/
|
|
210
|
+
setPlayerName(name: string): void;
|
|
211
|
+
/**
|
|
212
|
+
* Clear all stored player identity data.
|
|
213
|
+
*/
|
|
214
|
+
clear(): void;
|
|
215
|
+
/**
|
|
216
|
+
* Check if player identity is stored.
|
|
217
|
+
*/
|
|
218
|
+
hasIdentity(): boolean;
|
|
219
|
+
/**
|
|
220
|
+
* Generate a UUID v4.
|
|
221
|
+
* Uses crypto.randomUUID() if available, otherwise falls back to a manual implementation.
|
|
222
|
+
*/
|
|
223
|
+
private generateUUID;
|
|
224
|
+
}
|
|
225
|
+
|
|
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 };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
KeeperBoardClient: () => KeeperBoardClient,
|
|
24
|
+
KeeperBoardError: () => KeeperBoardError,
|
|
25
|
+
PlayerIdentity: () => PlayerIdentity
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/types.ts
|
|
30
|
+
var KeeperBoardError = class extends Error {
|
|
31
|
+
constructor(message, code, statusCode) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.code = code;
|
|
34
|
+
this.statusCode = statusCode;
|
|
35
|
+
this.name = "KeeperBoardError";
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// src/KeeperBoardClient.ts
|
|
40
|
+
var KeeperBoardClient = class {
|
|
41
|
+
constructor(config) {
|
|
42
|
+
this.apiUrl = config.apiUrl.replace(/\/$/, "");
|
|
43
|
+
this.apiKey = config.apiKey;
|
|
44
|
+
}
|
|
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) {
|
|
57
|
+
const params = new URLSearchParams();
|
|
58
|
+
if (leaderboardSlug) {
|
|
59
|
+
params.set("leaderboard", leaderboardSlug);
|
|
60
|
+
}
|
|
61
|
+
const url = `${this.apiUrl}/api/v1/scores${params.toString() ? "?" + params.toString() : ""}`;
|
|
62
|
+
const body = {
|
|
63
|
+
player_guid: playerGuid,
|
|
64
|
+
player_name: playerName,
|
|
65
|
+
score,
|
|
66
|
+
...metadata && { metadata }
|
|
67
|
+
};
|
|
68
|
+
const response = await this.request(url, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
body: JSON.stringify(body)
|
|
71
|
+
});
|
|
72
|
+
return response;
|
|
73
|
+
}
|
|
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) {
|
|
83
|
+
const params = new URLSearchParams();
|
|
84
|
+
params.set("limit", String(Math.min(limit, 100)));
|
|
85
|
+
params.set("offset", String(offset));
|
|
86
|
+
if (leaderboardSlug) {
|
|
87
|
+
params.set("leaderboard", leaderboardSlug);
|
|
88
|
+
}
|
|
89
|
+
const url = `${this.apiUrl}/api/v1/leaderboard?${params.toString()}`;
|
|
90
|
+
return this.request(url, {
|
|
91
|
+
method: "GET"
|
|
92
|
+
});
|
|
93
|
+
}
|
|
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) {
|
|
102
|
+
const params = new URLSearchParams();
|
|
103
|
+
if (leaderboardSlug) {
|
|
104
|
+
params.set("leaderboard", leaderboardSlug);
|
|
105
|
+
}
|
|
106
|
+
const url = `${this.apiUrl}/api/v1/player/${encodeURIComponent(playerGuid)}${params.toString() ? "?" + params.toString() : ""}`;
|
|
107
|
+
try {
|
|
108
|
+
return await this.request(url, {
|
|
109
|
+
method: "GET"
|
|
110
|
+
});
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (error instanceof KeeperBoardError && error.code === "NOT_FOUND") {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
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) {
|
|
127
|
+
const params = new URLSearchParams();
|
|
128
|
+
if (leaderboardSlug) {
|
|
129
|
+
params.set("leaderboard", leaderboardSlug);
|
|
130
|
+
}
|
|
131
|
+
const url = `${this.apiUrl}/api/v1/player/${encodeURIComponent(playerGuid)}${params.toString() ? "?" + params.toString() : ""}`;
|
|
132
|
+
return this.request(url, {
|
|
133
|
+
method: "PUT",
|
|
134
|
+
body: JSON.stringify({ player_name: newName })
|
|
135
|
+
});
|
|
136
|
+
}
|
|
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) {
|
|
147
|
+
const params = new URLSearchParams();
|
|
148
|
+
if (leaderboardSlug) {
|
|
149
|
+
params.set("leaderboard", leaderboardSlug);
|
|
150
|
+
}
|
|
151
|
+
const url = `${this.apiUrl}/api/v1/claim${params.toString() ? "?" + params.toString() : ""}`;
|
|
152
|
+
return this.request(url, {
|
|
153
|
+
method: "POST",
|
|
154
|
+
body: JSON.stringify({
|
|
155
|
+
player_guid: playerGuid,
|
|
156
|
+
player_name: playerName
|
|
157
|
+
})
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Check if the API is healthy.
|
|
162
|
+
* This endpoint does not require an API key.
|
|
163
|
+
*
|
|
164
|
+
* @returns Health status with version and timestamp
|
|
165
|
+
*/
|
|
166
|
+
async healthCheck() {
|
|
167
|
+
const url = `${this.apiUrl}/api/v1/health`;
|
|
168
|
+
const response = await fetch(url, {
|
|
169
|
+
method: "GET",
|
|
170
|
+
headers: {
|
|
171
|
+
Accept: "application/json"
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
const json = await response.json();
|
|
175
|
+
if (!json.success) {
|
|
176
|
+
throw new KeeperBoardError(json.error, json.code, response.status);
|
|
177
|
+
}
|
|
178
|
+
return json.data;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Internal request helper with auth and error handling.
|
|
182
|
+
*/
|
|
183
|
+
async request(url, options) {
|
|
184
|
+
const headers = {
|
|
185
|
+
"Content-Type": "application/json",
|
|
186
|
+
Accept: "application/json",
|
|
187
|
+
"X-API-Key": this.apiKey
|
|
188
|
+
};
|
|
189
|
+
const response = await fetch(url, {
|
|
190
|
+
...options,
|
|
191
|
+
headers: {
|
|
192
|
+
...headers,
|
|
193
|
+
...options.headers || {}
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
const json = await response.json();
|
|
197
|
+
if (!json.success) {
|
|
198
|
+
throw new KeeperBoardError(json.error, json.code, response.status);
|
|
199
|
+
}
|
|
200
|
+
return json.data;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// src/PlayerIdentity.ts
|
|
205
|
+
var DEFAULT_KEY_PREFIX = "keeperboard_";
|
|
206
|
+
var PlayerIdentity = class {
|
|
207
|
+
constructor(config = {}) {
|
|
208
|
+
this.keyPrefix = config.keyPrefix ?? DEFAULT_KEY_PREFIX;
|
|
209
|
+
this.guidKey = `${this.keyPrefix}player_guid`;
|
|
210
|
+
this.nameKey = `${this.keyPrefix}player_name`;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Get the stored player GUID, or null if none exists.
|
|
214
|
+
*/
|
|
215
|
+
getPlayerGuid() {
|
|
216
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
return localStorage.getItem(this.guidKey);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Set the player GUID in localStorage.
|
|
223
|
+
*/
|
|
224
|
+
setPlayerGuid(guid) {
|
|
225
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
localStorage.setItem(this.guidKey, guid);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Get the stored player GUID, creating one if it doesn't exist.
|
|
232
|
+
* Uses crypto.randomUUID() for generating new GUIDs.
|
|
233
|
+
*/
|
|
234
|
+
getOrCreatePlayerGuid() {
|
|
235
|
+
let guid = this.getPlayerGuid();
|
|
236
|
+
if (!guid) {
|
|
237
|
+
guid = this.generateUUID();
|
|
238
|
+
this.setPlayerGuid(guid);
|
|
239
|
+
}
|
|
240
|
+
return guid;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Get the stored player name, or null if none exists.
|
|
244
|
+
*/
|
|
245
|
+
getPlayerName() {
|
|
246
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
return localStorage.getItem(this.nameKey);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Set the player name in localStorage.
|
|
253
|
+
*/
|
|
254
|
+
setPlayerName(name) {
|
|
255
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
localStorage.setItem(this.nameKey, name);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Clear all stored player identity data.
|
|
262
|
+
*/
|
|
263
|
+
clear() {
|
|
264
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
localStorage.removeItem(this.guidKey);
|
|
268
|
+
localStorage.removeItem(this.nameKey);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Check if player identity is stored.
|
|
272
|
+
*/
|
|
273
|
+
hasIdentity() {
|
|
274
|
+
return this.getPlayerGuid() !== null;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Generate a UUID v4.
|
|
278
|
+
* Uses crypto.randomUUID() if available, otherwise falls back to a manual implementation.
|
|
279
|
+
*/
|
|
280
|
+
generateUUID() {
|
|
281
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
282
|
+
return crypto.randomUUID();
|
|
283
|
+
}
|
|
284
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
285
|
+
const r = Math.random() * 16 | 0;
|
|
286
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
287
|
+
return v.toString(16);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
292
|
+
0 && (module.exports = {
|
|
293
|
+
KeeperBoardClient,
|
|
294
|
+
KeeperBoardError,
|
|
295
|
+
PlayerIdentity
|
|
296
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var KeeperBoardError = class extends Error {
|
|
3
|
+
constructor(message, code, statusCode) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.statusCode = statusCode;
|
|
7
|
+
this.name = "KeeperBoardError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/KeeperBoardClient.ts
|
|
12
|
+
var KeeperBoardClient = class {
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.apiUrl = config.apiUrl.replace(/\/$/, "");
|
|
15
|
+
this.apiKey = config.apiKey;
|
|
16
|
+
}
|
|
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) {
|
|
29
|
+
const params = new URLSearchParams();
|
|
30
|
+
if (leaderboardSlug) {
|
|
31
|
+
params.set("leaderboard", leaderboardSlug);
|
|
32
|
+
}
|
|
33
|
+
const url = `${this.apiUrl}/api/v1/scores${params.toString() ? "?" + params.toString() : ""}`;
|
|
34
|
+
const body = {
|
|
35
|
+
player_guid: playerGuid,
|
|
36
|
+
player_name: playerName,
|
|
37
|
+
score,
|
|
38
|
+
...metadata && { metadata }
|
|
39
|
+
};
|
|
40
|
+
const response = await this.request(url, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
body: JSON.stringify(body)
|
|
43
|
+
});
|
|
44
|
+
return response;
|
|
45
|
+
}
|
|
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) {
|
|
55
|
+
const params = new URLSearchParams();
|
|
56
|
+
params.set("limit", String(Math.min(limit, 100)));
|
|
57
|
+
params.set("offset", String(offset));
|
|
58
|
+
if (leaderboardSlug) {
|
|
59
|
+
params.set("leaderboard", leaderboardSlug);
|
|
60
|
+
}
|
|
61
|
+
const url = `${this.apiUrl}/api/v1/leaderboard?${params.toString()}`;
|
|
62
|
+
return this.request(url, {
|
|
63
|
+
method: "GET"
|
|
64
|
+
});
|
|
65
|
+
}
|
|
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) {
|
|
74
|
+
const params = new URLSearchParams();
|
|
75
|
+
if (leaderboardSlug) {
|
|
76
|
+
params.set("leaderboard", leaderboardSlug);
|
|
77
|
+
}
|
|
78
|
+
const url = `${this.apiUrl}/api/v1/player/${encodeURIComponent(playerGuid)}${params.toString() ? "?" + params.toString() : ""}`;
|
|
79
|
+
try {
|
|
80
|
+
return await this.request(url, {
|
|
81
|
+
method: "GET"
|
|
82
|
+
});
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (error instanceof KeeperBoardError && error.code === "NOT_FOUND") {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
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) {
|
|
99
|
+
const params = new URLSearchParams();
|
|
100
|
+
if (leaderboardSlug) {
|
|
101
|
+
params.set("leaderboard", leaderboardSlug);
|
|
102
|
+
}
|
|
103
|
+
const url = `${this.apiUrl}/api/v1/player/${encodeURIComponent(playerGuid)}${params.toString() ? "?" + params.toString() : ""}`;
|
|
104
|
+
return this.request(url, {
|
|
105
|
+
method: "PUT",
|
|
106
|
+
body: JSON.stringify({ player_name: newName })
|
|
107
|
+
});
|
|
108
|
+
}
|
|
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) {
|
|
119
|
+
const params = new URLSearchParams();
|
|
120
|
+
if (leaderboardSlug) {
|
|
121
|
+
params.set("leaderboard", leaderboardSlug);
|
|
122
|
+
}
|
|
123
|
+
const url = `${this.apiUrl}/api/v1/claim${params.toString() ? "?" + params.toString() : ""}`;
|
|
124
|
+
return this.request(url, {
|
|
125
|
+
method: "POST",
|
|
126
|
+
body: JSON.stringify({
|
|
127
|
+
player_guid: playerGuid,
|
|
128
|
+
player_name: playerName
|
|
129
|
+
})
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Check if the API is healthy.
|
|
134
|
+
* This endpoint does not require an API key.
|
|
135
|
+
*
|
|
136
|
+
* @returns Health status with version and timestamp
|
|
137
|
+
*/
|
|
138
|
+
async healthCheck() {
|
|
139
|
+
const url = `${this.apiUrl}/api/v1/health`;
|
|
140
|
+
const response = await fetch(url, {
|
|
141
|
+
method: "GET",
|
|
142
|
+
headers: {
|
|
143
|
+
Accept: "application/json"
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
const json = await response.json();
|
|
147
|
+
if (!json.success) {
|
|
148
|
+
throw new KeeperBoardError(json.error, json.code, response.status);
|
|
149
|
+
}
|
|
150
|
+
return json.data;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Internal request helper with auth and error handling.
|
|
154
|
+
*/
|
|
155
|
+
async request(url, options) {
|
|
156
|
+
const headers = {
|
|
157
|
+
"Content-Type": "application/json",
|
|
158
|
+
Accept: "application/json",
|
|
159
|
+
"X-API-Key": this.apiKey
|
|
160
|
+
};
|
|
161
|
+
const response = await fetch(url, {
|
|
162
|
+
...options,
|
|
163
|
+
headers: {
|
|
164
|
+
...headers,
|
|
165
|
+
...options.headers || {}
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
const json = await response.json();
|
|
169
|
+
if (!json.success) {
|
|
170
|
+
throw new KeeperBoardError(json.error, json.code, response.status);
|
|
171
|
+
}
|
|
172
|
+
return json.data;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// src/PlayerIdentity.ts
|
|
177
|
+
var DEFAULT_KEY_PREFIX = "keeperboard_";
|
|
178
|
+
var PlayerIdentity = class {
|
|
179
|
+
constructor(config = {}) {
|
|
180
|
+
this.keyPrefix = config.keyPrefix ?? DEFAULT_KEY_PREFIX;
|
|
181
|
+
this.guidKey = `${this.keyPrefix}player_guid`;
|
|
182
|
+
this.nameKey = `${this.keyPrefix}player_name`;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Get the stored player GUID, or null if none exists.
|
|
186
|
+
*/
|
|
187
|
+
getPlayerGuid() {
|
|
188
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return localStorage.getItem(this.guidKey);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Set the player GUID in localStorage.
|
|
195
|
+
*/
|
|
196
|
+
setPlayerGuid(guid) {
|
|
197
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
localStorage.setItem(this.guidKey, guid);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Get the stored player GUID, creating one if it doesn't exist.
|
|
204
|
+
* Uses crypto.randomUUID() for generating new GUIDs.
|
|
205
|
+
*/
|
|
206
|
+
getOrCreatePlayerGuid() {
|
|
207
|
+
let guid = this.getPlayerGuid();
|
|
208
|
+
if (!guid) {
|
|
209
|
+
guid = this.generateUUID();
|
|
210
|
+
this.setPlayerGuid(guid);
|
|
211
|
+
}
|
|
212
|
+
return guid;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Get the stored player name, or null if none exists.
|
|
216
|
+
*/
|
|
217
|
+
getPlayerName() {
|
|
218
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
return localStorage.getItem(this.nameKey);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Set the player name in localStorage.
|
|
225
|
+
*/
|
|
226
|
+
setPlayerName(name) {
|
|
227
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
localStorage.setItem(this.nameKey, name);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Clear all stored player identity data.
|
|
234
|
+
*/
|
|
235
|
+
clear() {
|
|
236
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
localStorage.removeItem(this.guidKey);
|
|
240
|
+
localStorage.removeItem(this.nameKey);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Check if player identity is stored.
|
|
244
|
+
*/
|
|
245
|
+
hasIdentity() {
|
|
246
|
+
return this.getPlayerGuid() !== null;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Generate a UUID v4.
|
|
250
|
+
* Uses crypto.randomUUID() if available, otherwise falls back to a manual implementation.
|
|
251
|
+
*/
|
|
252
|
+
generateUUID() {
|
|
253
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
254
|
+
return crypto.randomUUID();
|
|
255
|
+
}
|
|
256
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
257
|
+
const r = Math.random() * 16 | 0;
|
|
258
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
259
|
+
return v.toString(16);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
export {
|
|
264
|
+
KeeperBoardClient,
|
|
265
|
+
KeeperBoardError,
|
|
266
|
+
PlayerIdentity
|
|
267
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "keeperboard-sdk",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "TypeScript client SDK for KeeperBoard leaderboard-as-a-service",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"clean": "rm -rf dist",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"leaderboard",
|
|
26
|
+
"phaser",
|
|
27
|
+
"game",
|
|
28
|
+
"scores",
|
|
29
|
+
"typescript",
|
|
30
|
+
"sdk",
|
|
31
|
+
"keeperboard"
|
|
32
|
+
],
|
|
33
|
+
"author": "Claude Roy",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git@github.com:clauderoy790/keeperboard.git",
|
|
38
|
+
"directory": "sdk"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/clauderoy790/keeperboard#readme",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/clauderoy790/keeperboard/issues"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"tsup": "^8.0.0",
|
|
46
|
+
"typescript": "^5.0.0"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18"
|
|
50
|
+
}
|
|
51
|
+
}
|