lightleaderboard 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,258 @@
1
+ # lightleaderboard
2
+
3
+ Official JavaScript / TypeScript SDK for [LightLeaderboard](https://leaderboard.goproso.com) — leaderboard-as-a-service for game developers.
4
+
5
+ Works in **Node.js 18+**, **browsers**, and any runtime with the Web Crypto API and native `fetch` (Deno, Bun, Cloudflare Workers, etc.).
6
+
7
+ ---
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install lightleaderboard
13
+ # or
14
+ yarn add lightleaderboard
15
+ # or
16
+ pnpm add lightleaderboard
17
+ ```
18
+
19
+ ---
20
+
21
+ ## Quick start
22
+
23
+ ```ts
24
+ import { LightLeaderboard } from 'lightleaderboard';
25
+
26
+ const lb = new LightLeaderboard({
27
+ apiKey: 'YOUR_API_KEY', // from the LightLeaderboard dashboard
28
+ gameId: 'YOUR_GAME_ID', // your game's reference ID
29
+ });
30
+
31
+ // Submit a score and get back the player's rank immediately
32
+ const result = await lb.submitScore({
33
+ score: 9500,
34
+ playerRefId: 'player-123',
35
+ playerName: 'Alice',
36
+ });
37
+ console.log(`Rank #${result.rank} of ${result.totalPlayers}!`);
38
+ // → "Rank #4 of 1024!"
39
+
40
+ // Fetch the top 10
41
+ const { entries } = await lb.getLeaderboard({ limit: 10 });
42
+ entries.forEach(e => {
43
+ console.log(`#${e.rank} ${e.playerName} ${e.score}`);
44
+ });
45
+ ```
46
+
47
+ ---
48
+
49
+ ## API reference
50
+
51
+ ### `new LightLeaderboard(config)`
52
+
53
+ | Option | Type | Required | Description |
54
+ |--------|------|----------|-------------|
55
+ | `apiKey` | `string` | ✓ | Your game's API key from the dashboard |
56
+ | `gameId` | `string` | ✓ | Your game's reference ID |
57
+ | `baseUrl` | `string` | | Override the API base URL |
58
+ | `scoreSecret` | `string` | | Auto-sign scores with HMAC-SHA256 (see [Score signing](#score-signing)) |
59
+
60
+ ---
61
+
62
+ ### `submitScore(options)`
63
+
64
+ Submit a score. Returns rank, personal-best flag, and total player count in a single call.
65
+
66
+ ```ts
67
+ const result = await lb.submitScore({
68
+ score: 9500,
69
+ playerRefId: 'player-123', // your internal player ID
70
+ playerName: 'Alice', // display name
71
+ playTimeMs: 120_000, // run duration
72
+ seasonId: 'season-3', // optional season bucket
73
+ teamId: 'team-red', // optional team bucket
74
+ submissionId: crypto.randomUUID(), // idempotency key
75
+ metadata: { level: 5, combo: 12 }, // arbitrary JSON
76
+ });
77
+
78
+ console.log(result.rank); // 4 (player's current rank)
79
+ console.log(result.isPersonalBest); // true
80
+ console.log(result.totalPlayers); // 1024
81
+ console.log(result.deduped); // true if submissionId was already seen
82
+ ```
83
+
84
+ **Response fields**
85
+
86
+ | Field | Type | Description |
87
+ |-------|------|-------------|
88
+ | `id` | `number` | Database ID of the created entry |
89
+ | `rank` | `number \| null` | Player's rank right after submission (`null` if no `playerRefId`) |
90
+ | `isPersonalBest` | `boolean` | Whether this beats the player's previous best |
91
+ | `totalPlayers` | `number \| null` | Total unique players on this leaderboard |
92
+ | `deduped` | `boolean?` | `true` when `submissionId` was already seen |
93
+
94
+ ---
95
+
96
+ ### `getLeaderboard(options?)`
97
+
98
+ Fetch the leaderboard. Returns **one entry per player** (their best score) by default.
99
+
100
+ ```ts
101
+ const { entries } = await lb.getLeaderboard({
102
+ limit: 20, // 1–100, default 20
103
+ offset: 0, // pagination
104
+ period: 'weekly', // 'all' | 'weekly' | 'monthly'
105
+ season: 'season-3',
106
+ team: 'team-red',
107
+ allEntries: false, // true → return every raw submission
108
+ });
109
+
110
+ // Each entry has a `rank` field (1-based, offset-aware)
111
+ entries.forEach(e => console.log(e.rank, e.playerName, e.score));
112
+ ```
113
+
114
+ **Pagination example**
115
+
116
+ ```ts
117
+ // Page 2 of a weekly board
118
+ const page2 = await lb.getLeaderboard({ limit: 20, offset: 20, period: 'weekly' });
119
+ ```
120
+
121
+ ---
122
+
123
+ ### `getPlayerRank(playerRefId, options?)`
124
+
125
+ Get a player's rank, score, and **percentile** in a single call.
126
+
127
+ ```ts
128
+ const rank = await lb.getPlayerRank('player-123', { period: 'weekly' });
129
+
130
+ console.log(rank.rank); // 4
131
+ console.log(rank.score); // 9500
132
+ console.log(rank.totalPlayers); // 1024
133
+ console.log(rank.percentile); // 99.7 → top 0.3%
134
+ ```
135
+
136
+ `percentile` is 0–100, higher is better. `rank 1 of 100` → `percentile 100`.
137
+
138
+ ---
139
+
140
+ ### `getCentricLeaderboard(playerRefId, options?)`
141
+
142
+ Fetch the leaderboard centered on a specific player — the entries immediately above and below them. Great for in-game "you vs. your neighbors" screens.
143
+
144
+ ```ts
145
+ const { entries, playerRank } = await lb.getCentricLeaderboard('player-123', {
146
+ limit: 11, // 5 above + player + 5 below
147
+ });
148
+ ```
149
+
150
+ ---
151
+
152
+ ### `getPlayer(playerRefId)`
153
+
154
+ Fetch a player's profile.
155
+
156
+ ```ts
157
+ const profile = await lb.getPlayer('player-123');
158
+ console.log(profile.playerName, profile.avatarUrl, profile.country);
159
+ ```
160
+
161
+ ---
162
+
163
+ ### `updatePlayer(playerRefId, options)`
164
+
165
+ Create or update a player's profile. Fields are merged — omitted fields keep their current value.
166
+
167
+ ```ts
168
+ await lb.updatePlayer('player-123', {
169
+ playerName: 'Alice',
170
+ avatarUrl: 'https://example.com/avatar.png',
171
+ country: 'US',
172
+ level: 42,
173
+ device: 'mobile',
174
+ });
175
+ ```
176
+
177
+ ---
178
+
179
+ ### `getPlayerScores(playerRefId, options?)`
180
+
181
+ Fetch all submissions a player has made, ordered **newest first**. Useful for progression graphs and run-history screens.
182
+
183
+ ```ts
184
+ const { entries, bestScore, total } = await lb.getPlayerScores('player-123', {
185
+ limit: 50, // 1–200, default 50
186
+ offset: 0,
187
+ });
188
+
189
+ console.log(`${total} total runs, best: ${bestScore}`);
190
+ entries.forEach(e => console.log(e.score, e.createdAt));
191
+ ```
192
+
193
+ ---
194
+
195
+ ## Score signing
196
+
197
+ If you enable "Require signed scores" on your game in the dashboard, every submission must include a valid HMAC-SHA256 signature. Pass `scoreSecret` to the constructor and the SDK handles signing automatically:
198
+
199
+ ```ts
200
+ const lb = new LightLeaderboard({
201
+ apiKey: 'YOUR_API_KEY',
202
+ gameId: 'YOUR_GAME_ID',
203
+ scoreSecret: 'YOUR_SCORE_SECRET', // from the dashboard → Signature Secret
204
+ });
205
+
206
+ // Scores are now automatically signed — no extra code needed
207
+ await lb.submitScore({ score: 9500, playerRefId: 'p1' });
208
+ ```
209
+
210
+ Signing uses the **Web Crypto API** (`crypto.subtle`), available natively in Node 18+, modern browsers, Bun, Deno, and Cloudflare Workers.
211
+
212
+ ---
213
+
214
+ ## Error handling
215
+
216
+ All methods throw `LightLeaderboardError` on failure.
217
+
218
+ ```ts
219
+ import { LightLeaderboard, LightLeaderboardError } from 'lightleaderboard';
220
+
221
+ try {
222
+ await lb.submitScore({ score: 9500 });
223
+ } catch (err) {
224
+ if (err instanceof LightLeaderboardError) {
225
+ console.error(err.message); // human-readable message from the API
226
+ console.error(err.status); // HTTP status code
227
+ console.error(err.response); // raw response body
228
+
229
+ if (err.isAuthError) console.error('Check your API key');
230
+ if (err.isRateLimitError) console.error('Slow down score submissions');
231
+ if (err.isBillingError) console.error('Free tier limit reached');
232
+ if (err.isValidationError) console.error('Invalid score data');
233
+ }
234
+ }
235
+ ```
236
+
237
+ ---
238
+
239
+ ## TypeScript
240
+
241
+ The SDK is written in TypeScript and ships full type declarations. All options and return types are exported:
242
+
243
+ ```ts
244
+ import type {
245
+ SubmitScoreOptions,
246
+ SubmitScoreResult,
247
+ GetLeaderboardOptions,
248
+ GetLeaderboardResult,
249
+ PlayerRankResult,
250
+ LeaderboardEntry,
251
+ } from 'lightleaderboard';
252
+ ```
253
+
254
+ ---
255
+
256
+ ## License
257
+
258
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,260 @@
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
+ LightLeaderboard: () => LightLeaderboard,
24
+ LightLeaderboardError: () => LightLeaderboardError
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/error.ts
29
+ var LightLeaderboardError = class extends Error {
30
+ constructor(message, status, response) {
31
+ super(message);
32
+ this.name = "LightLeaderboardError";
33
+ this.status = status;
34
+ this.response = response;
35
+ Object.setPrototypeOf(this, new.target.prototype);
36
+ }
37
+ get isAuthError() {
38
+ return this.status === 401 || this.status === 403;
39
+ }
40
+ get isRateLimitError() {
41
+ return this.status === 429;
42
+ }
43
+ get isBillingError() {
44
+ return this.status === 402;
45
+ }
46
+ get isValidationError() {
47
+ return this.status === 400;
48
+ }
49
+ };
50
+
51
+ // src/client.ts
52
+ var DEFAULT_BASE_URL = "https://leaderboard.goproso.com";
53
+ async function hmacSha256(key, data) {
54
+ const enc = new TextEncoder();
55
+ const cryptoKey = await globalThis.crypto.subtle.importKey(
56
+ "raw",
57
+ enc.encode(key),
58
+ { name: "HMAC", hash: "SHA-256" },
59
+ false,
60
+ ["sign"]
61
+ );
62
+ const sig = await globalThis.crypto.subtle.sign("HMAC", cryptoKey, enc.encode(data));
63
+ return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
64
+ }
65
+ function buildUrl(base, path, params) {
66
+ const url = `${base}${path}`;
67
+ if (!params) return url;
68
+ const sp = new URLSearchParams();
69
+ for (const [k, v] of Object.entries(params)) {
70
+ if (v !== void 0 && v !== null) sp.set(k, String(v));
71
+ }
72
+ const qs = sp.toString();
73
+ return qs ? `${url}?${qs}` : url;
74
+ }
75
+ var LightLeaderboard = class {
76
+ constructor(config) {
77
+ if (!config.apiKey) throw new Error("LightLeaderboard: apiKey is required");
78
+ if (!config.gameId) throw new Error("LightLeaderboard: gameId is required");
79
+ this.apiKey = config.apiKey;
80
+ this.gameId = config.gameId;
81
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
82
+ this.scoreSecret = config.scoreSecret;
83
+ }
84
+ gameUrl(path, params) {
85
+ return buildUrl(this.baseUrl, `/api/v1/games/${encodeURIComponent(this.gameId)}${path}`, params);
86
+ }
87
+ async fetch(method, url, body, extraHeaders) {
88
+ const headers = {
89
+ Authorization: `Bearer ${this.apiKey}`,
90
+ "Content-Type": "application/json",
91
+ ...extraHeaders
92
+ };
93
+ const res = await globalThis.fetch(url, {
94
+ method,
95
+ headers,
96
+ body: body !== void 0 ? JSON.stringify(body) : void 0
97
+ });
98
+ let data;
99
+ try {
100
+ data = await res.json();
101
+ } catch {
102
+ throw new LightLeaderboardError(`HTTP ${res.status}: failed to parse response`, res.status, null);
103
+ }
104
+ if (!res.ok || data?.ok === false) {
105
+ throw new LightLeaderboardError(
106
+ data?.error ?? `HTTP ${res.status}`,
107
+ res.status,
108
+ data
109
+ );
110
+ }
111
+ return data;
112
+ }
113
+ // ─── Score submission ───────────────────────────────────────────────────────
114
+ /**
115
+ * Submit a score to the leaderboard.
116
+ *
117
+ * Returns the player's new rank immediately — no second API call needed.
118
+ *
119
+ * @example
120
+ * const result = await lb.submitScore({ score: 9500, playerRefId: 'p1', playerName: 'Alice' });
121
+ * console.log(`Rank #${result.rank} of ${result.totalPlayers}`);
122
+ */
123
+ async submitScore(options) {
124
+ const url = this.gameUrl("/scores");
125
+ const bodyStr = JSON.stringify(options);
126
+ const extraHeaders = {};
127
+ if (this.scoreSecret) {
128
+ const sig = await hmacSha256(this.scoreSecret, bodyStr);
129
+ extraHeaders["x-score-signature"] = `sha256=${sig}`;
130
+ }
131
+ const res = await globalThis.fetch(url, {
132
+ method: "POST",
133
+ headers: {
134
+ Authorization: `Bearer ${this.apiKey}`,
135
+ "Content-Type": "application/json",
136
+ ...extraHeaders
137
+ },
138
+ body: bodyStr
139
+ });
140
+ let data;
141
+ try {
142
+ data = await res.json();
143
+ } catch {
144
+ throw new LightLeaderboardError(`HTTP ${res.status}: failed to parse response`, res.status, null);
145
+ }
146
+ if (!res.ok || data?.ok === false) {
147
+ throw new LightLeaderboardError(data?.error ?? `HTTP ${res.status}`, res.status, data);
148
+ }
149
+ return {
150
+ id: data.id,
151
+ rank: data.rank ?? null,
152
+ isPersonalBest: data.isPersonalBest ?? false,
153
+ totalPlayers: data.totalPlayers ?? null,
154
+ deduped: data.deduped
155
+ };
156
+ }
157
+ // ─── Leaderboard ───────────────────────────────────────────────────────────
158
+ /**
159
+ * Fetch the leaderboard. Returns one entry per player (their best score) by default.
160
+ *
161
+ * @example
162
+ * const { entries } = await lb.getLeaderboard({ limit: 10, period: 'weekly' });
163
+ * entries.forEach(e => console.log(`#${e.rank} ${e.playerName}: ${e.score}`));
164
+ */
165
+ async getLeaderboard(options) {
166
+ const { allEntries, ...rest } = options ?? {};
167
+ const data = await this.fetch(
168
+ "GET",
169
+ this.gameUrl("/leaderboard", {
170
+ ...rest,
171
+ allEntries: allEntries ? "true" : void 0
172
+ })
173
+ );
174
+ return {
175
+ entries: data.entries,
176
+ scoreOrder: data.scoreOrder,
177
+ period: data.period,
178
+ season: data.season ?? null,
179
+ team: data.team ?? null,
180
+ limit: data.limit,
181
+ offset: data.offset
182
+ };
183
+ }
184
+ // ─── Rank ───────────────────────────────────────────────────────────────────
185
+ /**
186
+ * Get a player's current rank, score, and percentile.
187
+ *
188
+ * @example
189
+ * const { rank, percentile } = await lb.getPlayerRank('player-123');
190
+ * console.log(`Rank #${rank} — top ${(100 - percentile).toFixed(1)}%`);
191
+ */
192
+ async getPlayerRank(playerRefId, options) {
193
+ return this.fetch(
194
+ "GET",
195
+ this.gameUrl(`/players/${encodeURIComponent(playerRefId)}/rank`, options)
196
+ );
197
+ }
198
+ // ─── Centric leaderboard ───────────────────────────────────────────────────
199
+ /**
200
+ * Fetch the leaderboard centered on a specific player, showing the players
201
+ * immediately above and below them.
202
+ *
203
+ * @example
204
+ * const { entries, playerRank } = await lb.getCentricLeaderboard('player-123', { limit: 11 });
205
+ */
206
+ async getCentricLeaderboard(playerRefId, options) {
207
+ const data = await this.fetch(
208
+ "GET",
209
+ this.gameUrl(`/players/${encodeURIComponent(playerRefId)}/centric`, options)
210
+ );
211
+ return {
212
+ entries: data.entries,
213
+ playerRank: data.playerRank ?? null,
214
+ period: data.period,
215
+ season: data.season ?? null,
216
+ team: data.team ?? null,
217
+ scoreOrder: data.scoreOrder
218
+ };
219
+ }
220
+ // ─── Player profile ─────────────────────────────────────────────────────────
221
+ /**
222
+ * Fetch a player's profile (name, avatar, country, level, etc.).
223
+ */
224
+ async getPlayer(playerRefId) {
225
+ const data = await this.fetch(
226
+ "GET",
227
+ this.gameUrl(`/players/${encodeURIComponent(playerRefId)}`)
228
+ );
229
+ return data.player;
230
+ }
231
+ /**
232
+ * Create or update a player's profile. Fields are merged — omitted fields
233
+ * keep their existing values.
234
+ *
235
+ * @example
236
+ * await lb.updatePlayer('player-123', { playerName: 'Alice', country: 'US', level: 5 });
237
+ */
238
+ async updatePlayer(playerRefId, options) {
239
+ await this.fetch(
240
+ "PUT",
241
+ this.gameUrl(`/players/${encodeURIComponent(playerRefId)}`),
242
+ options
243
+ );
244
+ }
245
+ // ─── Score history ──────────────────────────────────────────────────────────
246
+ /**
247
+ * Fetch all submissions a player has ever made, ordered newest first.
248
+ * Useful for progression charts and run history screens.
249
+ *
250
+ * @example
251
+ * const { entries, bestScore, total } = await lb.getPlayerScores('player-123');
252
+ */
253
+ async getPlayerScores(playerRefId, options) {
254
+ return this.fetch(
255
+ "GET",
256
+ this.gameUrl(`/players/${encodeURIComponent(playerRefId)}/scores`, options)
257
+ );
258
+ }
259
+ };
260
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/error.ts","../src/client.ts"],"sourcesContent":["export { LightLeaderboard } from './client.js';\nexport { LightLeaderboardError } from './error.js';\nexport type {\n LightLeaderboardConfig,\n LeaderboardEntry,\n GetLeaderboardOptions,\n GetLeaderboardResult,\n SubmitScoreOptions,\n SubmitScoreResult,\n GetRankOptions,\n PlayerRankResult,\n GetCentricOptions,\n CentricLeaderboardResult,\n PlayerProfile,\n UpdatePlayerOptions,\n PlayerScoreEntry,\n GetPlayerScoresOptions,\n GetPlayerScoresResult,\n} from './types.js';\n","export class LightLeaderboardError extends Error {\n /** HTTP status code returned by the API */\n readonly status: number;\n /** Raw response body from the API */\n readonly response: unknown;\n\n constructor(message: string, status: number, response: unknown) {\n super(message);\n this.name = 'LightLeaderboardError';\n this.status = status;\n this.response = response;\n // Restore prototype chain in environments that need it\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n get isAuthError() {\n return this.status === 401 || this.status === 403;\n }\n\n get isRateLimitError() {\n return this.status === 429;\n }\n\n get isBillingError() {\n return this.status === 402;\n }\n\n get isValidationError() {\n return this.status === 400;\n }\n}\n","import { LightLeaderboardError } from './error.js';\nimport type {\n LightLeaderboardConfig,\n SubmitScoreOptions,\n SubmitScoreResult,\n GetLeaderboardOptions,\n GetLeaderboardResult,\n GetRankOptions,\n PlayerRankResult,\n GetCentricOptions,\n CentricLeaderboardResult,\n PlayerProfile,\n UpdatePlayerOptions,\n GetPlayerScoresOptions,\n GetPlayerScoresResult,\n} from './types.js';\n\nconst DEFAULT_BASE_URL = 'https://leaderboard.goproso.com';\n\nasync function hmacSha256(key: string, data: string): Promise<string> {\n const enc = new TextEncoder();\n const cryptoKey = await globalThis.crypto.subtle.importKey(\n 'raw',\n enc.encode(key),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign']\n );\n const sig = await globalThis.crypto.subtle.sign('HMAC', cryptoKey, enc.encode(data));\n return Array.from(new Uint8Array(sig))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n}\n\nfunction buildUrl(base: string, path: string, params?: object): string {\n const url = `${base}${path}`;\n if (!params) return url;\n const sp = new URLSearchParams();\n for (const [k, v] of Object.entries(params)) {\n if (v !== undefined && v !== null) sp.set(k, String(v));\n }\n const qs = sp.toString();\n return qs ? `${url}?${qs}` : url;\n}\n\nexport class LightLeaderboard {\n private readonly apiKey: string;\n private readonly gameId: string;\n private readonly baseUrl: string;\n private readonly scoreSecret?: string;\n\n constructor(config: LightLeaderboardConfig) {\n if (!config.apiKey) throw new Error('LightLeaderboard: apiKey is required');\n if (!config.gameId) throw new Error('LightLeaderboard: gameId is required');\n this.apiKey = config.apiKey;\n this.gameId = config.gameId;\n this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, '');\n this.scoreSecret = config.scoreSecret;\n }\n\n private gameUrl(path: string, params?: object): string {\n return buildUrl(this.baseUrl, `/api/v1/games/${encodeURIComponent(this.gameId)}${path}`, params);\n }\n\n private async fetch<T>(\n method: string,\n url: string,\n body?: unknown,\n extraHeaders?: Record<string, string>\n ): Promise<T> {\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json',\n ...extraHeaders,\n };\n\n const res = await globalThis.fetch(url, {\n method,\n headers,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n });\n\n let data: any;\n try {\n data = await res.json();\n } catch {\n throw new LightLeaderboardError(`HTTP ${res.status}: failed to parse response`, res.status, null);\n }\n\n if (!res.ok || data?.ok === false) {\n throw new LightLeaderboardError(\n data?.error ?? `HTTP ${res.status}`,\n res.status,\n data\n );\n }\n\n return data as T;\n }\n\n // ─── Score submission ───────────────────────────────────────────────────────\n\n /**\n * Submit a score to the leaderboard.\n *\n * Returns the player's new rank immediately — no second API call needed.\n *\n * @example\n * const result = await lb.submitScore({ score: 9500, playerRefId: 'p1', playerName: 'Alice' });\n * console.log(`Rank #${result.rank} of ${result.totalPlayers}`);\n */\n async submitScore(options: SubmitScoreOptions): Promise<SubmitScoreResult> {\n const url = this.gameUrl('/scores');\n const bodyStr = JSON.stringify(options);\n const extraHeaders: Record<string, string> = {};\n\n if (this.scoreSecret) {\n const sig = await hmacSha256(this.scoreSecret, bodyStr);\n extraHeaders['x-score-signature'] = `sha256=${sig}`;\n }\n\n const res = await globalThis.fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json',\n ...extraHeaders,\n },\n body: bodyStr,\n });\n\n let data: any;\n try {\n data = await res.json();\n } catch {\n throw new LightLeaderboardError(`HTTP ${res.status}: failed to parse response`, res.status, null);\n }\n\n if (!res.ok || data?.ok === false) {\n throw new LightLeaderboardError(data?.error ?? `HTTP ${res.status}`, res.status, data);\n }\n\n return {\n id: data.id,\n rank: data.rank ?? null,\n isPersonalBest: data.isPersonalBest ?? false,\n totalPlayers: data.totalPlayers ?? null,\n deduped: data.deduped,\n };\n }\n\n // ─── Leaderboard ───────────────────────────────────────────────────────────\n\n /**\n * Fetch the leaderboard. Returns one entry per player (their best score) by default.\n *\n * @example\n * const { entries } = await lb.getLeaderboard({ limit: 10, period: 'weekly' });\n * entries.forEach(e => console.log(`#${e.rank} ${e.playerName}: ${e.score}`));\n */\n async getLeaderboard(options?: GetLeaderboardOptions): Promise<GetLeaderboardResult> {\n const { allEntries, ...rest } = options ?? {};\n const data = await this.fetch<any>(\n 'GET',\n this.gameUrl('/leaderboard', {\n ...rest,\n allEntries: allEntries ? 'true' : undefined,\n })\n );\n return {\n entries: data.entries,\n scoreOrder: data.scoreOrder,\n period: data.period,\n season: data.season ?? null,\n team: data.team ?? null,\n limit: data.limit,\n offset: data.offset,\n };\n }\n\n // ─── Rank ───────────────────────────────────────────────────────────────────\n\n /**\n * Get a player's current rank, score, and percentile.\n *\n * @example\n * const { rank, percentile } = await lb.getPlayerRank('player-123');\n * console.log(`Rank #${rank} — top ${(100 - percentile).toFixed(1)}%`);\n */\n async getPlayerRank(playerRefId: string, options?: GetRankOptions): Promise<PlayerRankResult> {\n return this.fetch<PlayerRankResult>(\n 'GET',\n this.gameUrl(`/players/${encodeURIComponent(playerRefId)}/rank`, options)\n );\n }\n\n // ─── Centric leaderboard ───────────────────────────────────────────────────\n\n /**\n * Fetch the leaderboard centered on a specific player, showing the players\n * immediately above and below them.\n *\n * @example\n * const { entries, playerRank } = await lb.getCentricLeaderboard('player-123', { limit: 11 });\n */\n async getCentricLeaderboard(\n playerRefId: string,\n options?: GetCentricOptions\n ): Promise<CentricLeaderboardResult> {\n const data = await this.fetch<any>(\n 'GET',\n this.gameUrl(`/players/${encodeURIComponent(playerRefId)}/centric`, options)\n );\n return {\n entries: data.entries,\n playerRank: data.playerRank ?? null,\n period: data.period,\n season: data.season ?? null,\n team: data.team ?? null,\n scoreOrder: data.scoreOrder,\n };\n }\n\n // ─── Player profile ─────────────────────────────────────────────────────────\n\n /**\n * Fetch a player's profile (name, avatar, country, level, etc.).\n */\n async getPlayer(playerRefId: string): Promise<PlayerProfile> {\n const data = await this.fetch<any>(\n 'GET',\n this.gameUrl(`/players/${encodeURIComponent(playerRefId)}`)\n );\n return data.player as PlayerProfile;\n }\n\n /**\n * Create or update a player's profile. Fields are merged — omitted fields\n * keep their existing values.\n *\n * @example\n * await lb.updatePlayer('player-123', { playerName: 'Alice', country: 'US', level: 5 });\n */\n async updatePlayer(playerRefId: string, options: UpdatePlayerOptions): Promise<void> {\n await this.fetch<any>(\n 'PUT',\n this.gameUrl(`/players/${encodeURIComponent(playerRefId)}`),\n options\n );\n }\n\n // ─── Score history ──────────────────────────────────────────────────────────\n\n /**\n * Fetch all submissions a player has ever made, ordered newest first.\n * Useful for progression charts and run history screens.\n *\n * @example\n * const { entries, bestScore, total } = await lb.getPlayerScores('player-123');\n */\n async getPlayerScores(\n playerRefId: string,\n options?: GetPlayerScoresOptions\n ): Promise<GetPlayerScoresResult> {\n return this.fetch<GetPlayerScoresResult>(\n 'GET',\n this.gameUrl(`/players/${encodeURIComponent(playerRefId)}/scores`, options)\n );\n }\n}\n"],"mappings":"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","names":[]}