scorezilla 0.1.0-next.3 → 0.3.0-next.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/CHANGELOG.md +184 -0
- package/README.md +13 -0
- package/dist/{errors-CUAQsaVS.d.cts → errors-B7hyC-C5.d.cts} +79 -5
- package/dist/{errors-CUAQsaVS.d.ts → errors-B7hyC-C5.d.ts} +79 -5
- package/dist/identity.cjs +87 -0
- package/dist/identity.cjs.map +1 -0
- package/dist/identity.d.cts +151 -0
- package/dist/identity.d.ts +151 -0
- package/dist/identity.js +82 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.cjs +96 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -6
- package/dist/index.d.ts +18 -6
- package/dist/index.js +96 -10
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +95 -9
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +19 -13
- package/dist/server.d.ts +19 -13
- package/dist/server.js +95 -9
- package/dist/server.js.map +1 -1
- package/package.json +14 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity preset helpers — opinionated, selectable ways for your game to
|
|
3
|
+
* generate or fetch a `playerId` for score submission.
|
|
4
|
+
*
|
|
5
|
+
* Background: every Scorezilla score carries an opaque `playerId`. The SDK
|
|
6
|
+
* doesn't care whether it's a UUID, a nickname, an email, or a server
|
|
7
|
+
* session token. But _how_ your game decides on that value is a UX +
|
|
8
|
+
* privacy decision the team should make explicitly. These presets are the
|
|
9
|
+
* blessed patterns; pick one per integration.
|
|
10
|
+
*
|
|
11
|
+
* See ADR 0003 (MCP identity axis) for the design rationale:
|
|
12
|
+
* https://github.com/isco-tec/scorezilla/blob/main/docs/adr/0003-mcp-identity-axis.md
|
|
13
|
+
*
|
|
14
|
+
* @module scorezilla/identity
|
|
15
|
+
* @since 0.3.0
|
|
16
|
+
*/
|
|
17
|
+
interface AnonymousPlayerOptions {
|
|
18
|
+
/** localStorage key under which the generated UUID is persisted. */
|
|
19
|
+
readonly storageKey: string;
|
|
20
|
+
}
|
|
21
|
+
interface PromptedPlayerOptions {
|
|
22
|
+
/** localStorage key under which the user-entered name is persisted. */
|
|
23
|
+
readonly storageKey: string;
|
|
24
|
+
/** Message shown in `window.prompt()` on first run. */
|
|
25
|
+
readonly prompt: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Identity handle returned by the storage-backed presets.
|
|
29
|
+
*
|
|
30
|
+
* `forget()` clears the persisted value from browser storage. It does
|
|
31
|
+
* **not** delete server-side score history for this player — to fully
|
|
32
|
+
* erase a player's data, call the admin "delete player" endpoint.
|
|
33
|
+
*/
|
|
34
|
+
interface PlayerHandle {
|
|
35
|
+
readonly id: string;
|
|
36
|
+
readonly forget: () => void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Marker returned by `useServerAuthoritative()` to signal that the
|
|
40
|
+
* game's backend (not the browser) owns the `playerId` via the
|
|
41
|
+
* HMAC-signed secure path (`scorezilla/server`).
|
|
42
|
+
*/
|
|
43
|
+
interface ServerAuthoritativeMarker {
|
|
44
|
+
readonly source: 'server-authoritative';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Anonymous player identity. Generates an opaque UUID on first run and
|
|
48
|
+
* persists it in `localStorage` so the same browser keeps the same ID
|
|
49
|
+
* across page reloads.
|
|
50
|
+
*
|
|
51
|
+
* **Privacy.** Stores a randomly-generated UUID in browser localStorage;
|
|
52
|
+
* the value is sent to the API on every score submission and persisted
|
|
53
|
+
* indefinitely in the player's score-history rows. No PII is collected.
|
|
54
|
+
* `forget()` removes the localStorage entry; for full server-side erasure
|
|
55
|
+
* call the admin "delete player" endpoint.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* import { Scorezilla } from 'scorezilla';
|
|
60
|
+
* import { useAnonymousPlayer } from 'scorezilla/identity';
|
|
61
|
+
*
|
|
62
|
+
* const player = useAnonymousPlayer({ storageKey: 'mygame:player' });
|
|
63
|
+
* const sz = new Scorezilla({ publicKey: 'pk_…' });
|
|
64
|
+
* await sz.submitScore({ boardId, playerId: player.id, score: 42 });
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @since 0.3.0
|
|
68
|
+
* @stability stable
|
|
69
|
+
*/
|
|
70
|
+
declare function useAnonymousPlayer(options: AnonymousPlayerOptions): PlayerHandle;
|
|
71
|
+
/**
|
|
72
|
+
* Prompted player identity. On first run shows a `window.prompt()` asking
|
|
73
|
+
* the user for a name, then persists it in `localStorage` for subsequent
|
|
74
|
+
* visits. Returns `null` if there is no browser (SSR), no `window.prompt`,
|
|
75
|
+
* or if the user cancelled / entered an empty value.
|
|
76
|
+
*
|
|
77
|
+
* **Privacy.** The user-entered string is stored in browser localStorage,
|
|
78
|
+
* transmitted to the API on every score submission, and persisted
|
|
79
|
+
* indefinitely on the leaderboard. The persisted value is whatever the
|
|
80
|
+
* user typed — sanitize at the UI layer if you care. `forget()` clears
|
|
81
|
+
* local state but does NOT delete server-side history.
|
|
82
|
+
*
|
|
83
|
+
* **UX caveat.** `window.prompt()` blocks the main thread and looks
|
|
84
|
+
* dated in modern apps. For a polished flow, build your own inline form
|
|
85
|
+
* and pass the result to `submitScore` directly — the preset is here to
|
|
86
|
+
* cover quick prototypes and jam-style integrations.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* import { Scorezilla } from 'scorezilla';
|
|
91
|
+
* import { usePromptedPlayer } from 'scorezilla/identity';
|
|
92
|
+
*
|
|
93
|
+
* const player = usePromptedPlayer({
|
|
94
|
+
* storageKey: 'mygame:player',
|
|
95
|
+
* prompt: 'Enter a name for the leaderboard:',
|
|
96
|
+
* });
|
|
97
|
+
*
|
|
98
|
+
* if (player) {
|
|
99
|
+
* const sz = new Scorezilla({ publicKey: 'pk_…' });
|
|
100
|
+
* await sz.submitScore({ boardId, playerId: player.id, score: 42 });
|
|
101
|
+
* }
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* @since 0.3.0
|
|
105
|
+
* @stability stable
|
|
106
|
+
*/
|
|
107
|
+
declare function usePromptedPlayer(options: PromptedPlayerOptions): PlayerHandle | null;
|
|
108
|
+
/**
|
|
109
|
+
* Server-authoritative identity marker. Signals that the game's backend
|
|
110
|
+
* is responsible for the `playerId` via the HMAC-signed secure path
|
|
111
|
+
* (`scorezilla/server`). The browser SDK does no identity work — the
|
|
112
|
+
* server picks the value, signs the submission, and posts.
|
|
113
|
+
*
|
|
114
|
+
* The return value is a no-op marker; you don't pass it anywhere. It
|
|
115
|
+
* exists so MCP-returned snippets can emit a single line that
|
|
116
|
+
* unambiguously says "this game uses the secure path; identity is
|
|
117
|
+
* server-authoritative."
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* // Client (no identity helper needed):
|
|
122
|
+
* import { useServerAuthoritative } from 'scorezilla/identity';
|
|
123
|
+
* useServerAuthoritative();
|
|
124
|
+
*
|
|
125
|
+
* // Server (where the real work happens):
|
|
126
|
+
* import { Scorezilla } from 'scorezilla/server';
|
|
127
|
+
* const sz = new Scorezilla({ secretKey: process.env.SCOREZILLA_SECRET_KEY! });
|
|
128
|
+
* await sz.submitScore({ boardId, playerId: serverDerivedId, score });
|
|
129
|
+
* ```
|
|
130
|
+
*
|
|
131
|
+
* @since 0.3.0
|
|
132
|
+
* @stability stable
|
|
133
|
+
*/
|
|
134
|
+
declare function useServerAuthoritative(): ServerAuthoritativeMarker;
|
|
135
|
+
/**
|
|
136
|
+
* OAuth-backed player identity. **Preview stub in 0.3.0-next.x** — throws
|
|
137
|
+
* on call. Full implementation (Google + GitHub for v1, Apple + Discord
|
|
138
|
+
* deferred) lands in a follow-up release on the `next` dist-tag, before
|
|
139
|
+
* the `latest` 0.3.0 ships.
|
|
140
|
+
*
|
|
141
|
+
* Until then: drive your own OAuth flow and pass the resulting user
|
|
142
|
+
* identifier to `submitScore` directly.
|
|
143
|
+
*
|
|
144
|
+
* @since 0.3.0
|
|
145
|
+
* @stability preview
|
|
146
|
+
*/
|
|
147
|
+
declare function useAuthProvider(_options: {
|
|
148
|
+
readonly provider: 'google' | 'github';
|
|
149
|
+
}): never;
|
|
150
|
+
|
|
151
|
+
export { type AnonymousPlayerOptions, type PlayerHandle, type PromptedPlayerOptions, type ServerAuthoritativeMarker, useAnonymousPlayer, useAuthProvider, usePromptedPlayer, useServerAuthoritative };
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity preset helpers — opinionated, selectable ways for your game to
|
|
3
|
+
* generate or fetch a `playerId` for score submission.
|
|
4
|
+
*
|
|
5
|
+
* Background: every Scorezilla score carries an opaque `playerId`. The SDK
|
|
6
|
+
* doesn't care whether it's a UUID, a nickname, an email, or a server
|
|
7
|
+
* session token. But _how_ your game decides on that value is a UX +
|
|
8
|
+
* privacy decision the team should make explicitly. These presets are the
|
|
9
|
+
* blessed patterns; pick one per integration.
|
|
10
|
+
*
|
|
11
|
+
* See ADR 0003 (MCP identity axis) for the design rationale:
|
|
12
|
+
* https://github.com/isco-tec/scorezilla/blob/main/docs/adr/0003-mcp-identity-axis.md
|
|
13
|
+
*
|
|
14
|
+
* @module scorezilla/identity
|
|
15
|
+
* @since 0.3.0
|
|
16
|
+
*/
|
|
17
|
+
interface AnonymousPlayerOptions {
|
|
18
|
+
/** localStorage key under which the generated UUID is persisted. */
|
|
19
|
+
readonly storageKey: string;
|
|
20
|
+
}
|
|
21
|
+
interface PromptedPlayerOptions {
|
|
22
|
+
/** localStorage key under which the user-entered name is persisted. */
|
|
23
|
+
readonly storageKey: string;
|
|
24
|
+
/** Message shown in `window.prompt()` on first run. */
|
|
25
|
+
readonly prompt: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Identity handle returned by the storage-backed presets.
|
|
29
|
+
*
|
|
30
|
+
* `forget()` clears the persisted value from browser storage. It does
|
|
31
|
+
* **not** delete server-side score history for this player — to fully
|
|
32
|
+
* erase a player's data, call the admin "delete player" endpoint.
|
|
33
|
+
*/
|
|
34
|
+
interface PlayerHandle {
|
|
35
|
+
readonly id: string;
|
|
36
|
+
readonly forget: () => void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Marker returned by `useServerAuthoritative()` to signal that the
|
|
40
|
+
* game's backend (not the browser) owns the `playerId` via the
|
|
41
|
+
* HMAC-signed secure path (`scorezilla/server`).
|
|
42
|
+
*/
|
|
43
|
+
interface ServerAuthoritativeMarker {
|
|
44
|
+
readonly source: 'server-authoritative';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Anonymous player identity. Generates an opaque UUID on first run and
|
|
48
|
+
* persists it in `localStorage` so the same browser keeps the same ID
|
|
49
|
+
* across page reloads.
|
|
50
|
+
*
|
|
51
|
+
* **Privacy.** Stores a randomly-generated UUID in browser localStorage;
|
|
52
|
+
* the value is sent to the API on every score submission and persisted
|
|
53
|
+
* indefinitely in the player's score-history rows. No PII is collected.
|
|
54
|
+
* `forget()` removes the localStorage entry; for full server-side erasure
|
|
55
|
+
* call the admin "delete player" endpoint.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* import { Scorezilla } from 'scorezilla';
|
|
60
|
+
* import { useAnonymousPlayer } from 'scorezilla/identity';
|
|
61
|
+
*
|
|
62
|
+
* const player = useAnonymousPlayer({ storageKey: 'mygame:player' });
|
|
63
|
+
* const sz = new Scorezilla({ publicKey: 'pk_…' });
|
|
64
|
+
* await sz.submitScore({ boardId, playerId: player.id, score: 42 });
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @since 0.3.0
|
|
68
|
+
* @stability stable
|
|
69
|
+
*/
|
|
70
|
+
declare function useAnonymousPlayer(options: AnonymousPlayerOptions): PlayerHandle;
|
|
71
|
+
/**
|
|
72
|
+
* Prompted player identity. On first run shows a `window.prompt()` asking
|
|
73
|
+
* the user for a name, then persists it in `localStorage` for subsequent
|
|
74
|
+
* visits. Returns `null` if there is no browser (SSR), no `window.prompt`,
|
|
75
|
+
* or if the user cancelled / entered an empty value.
|
|
76
|
+
*
|
|
77
|
+
* **Privacy.** The user-entered string is stored in browser localStorage,
|
|
78
|
+
* transmitted to the API on every score submission, and persisted
|
|
79
|
+
* indefinitely on the leaderboard. The persisted value is whatever the
|
|
80
|
+
* user typed — sanitize at the UI layer if you care. `forget()` clears
|
|
81
|
+
* local state but does NOT delete server-side history.
|
|
82
|
+
*
|
|
83
|
+
* **UX caveat.** `window.prompt()` blocks the main thread and looks
|
|
84
|
+
* dated in modern apps. For a polished flow, build your own inline form
|
|
85
|
+
* and pass the result to `submitScore` directly — the preset is here to
|
|
86
|
+
* cover quick prototypes and jam-style integrations.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* import { Scorezilla } from 'scorezilla';
|
|
91
|
+
* import { usePromptedPlayer } from 'scorezilla/identity';
|
|
92
|
+
*
|
|
93
|
+
* const player = usePromptedPlayer({
|
|
94
|
+
* storageKey: 'mygame:player',
|
|
95
|
+
* prompt: 'Enter a name for the leaderboard:',
|
|
96
|
+
* });
|
|
97
|
+
*
|
|
98
|
+
* if (player) {
|
|
99
|
+
* const sz = new Scorezilla({ publicKey: 'pk_…' });
|
|
100
|
+
* await sz.submitScore({ boardId, playerId: player.id, score: 42 });
|
|
101
|
+
* }
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* @since 0.3.0
|
|
105
|
+
* @stability stable
|
|
106
|
+
*/
|
|
107
|
+
declare function usePromptedPlayer(options: PromptedPlayerOptions): PlayerHandle | null;
|
|
108
|
+
/**
|
|
109
|
+
* Server-authoritative identity marker. Signals that the game's backend
|
|
110
|
+
* is responsible for the `playerId` via the HMAC-signed secure path
|
|
111
|
+
* (`scorezilla/server`). The browser SDK does no identity work — the
|
|
112
|
+
* server picks the value, signs the submission, and posts.
|
|
113
|
+
*
|
|
114
|
+
* The return value is a no-op marker; you don't pass it anywhere. It
|
|
115
|
+
* exists so MCP-returned snippets can emit a single line that
|
|
116
|
+
* unambiguously says "this game uses the secure path; identity is
|
|
117
|
+
* server-authoritative."
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* // Client (no identity helper needed):
|
|
122
|
+
* import { useServerAuthoritative } from 'scorezilla/identity';
|
|
123
|
+
* useServerAuthoritative();
|
|
124
|
+
*
|
|
125
|
+
* // Server (where the real work happens):
|
|
126
|
+
* import { Scorezilla } from 'scorezilla/server';
|
|
127
|
+
* const sz = new Scorezilla({ secretKey: process.env.SCOREZILLA_SECRET_KEY! });
|
|
128
|
+
* await sz.submitScore({ boardId, playerId: serverDerivedId, score });
|
|
129
|
+
* ```
|
|
130
|
+
*
|
|
131
|
+
* @since 0.3.0
|
|
132
|
+
* @stability stable
|
|
133
|
+
*/
|
|
134
|
+
declare function useServerAuthoritative(): ServerAuthoritativeMarker;
|
|
135
|
+
/**
|
|
136
|
+
* OAuth-backed player identity. **Preview stub in 0.3.0-next.x** — throws
|
|
137
|
+
* on call. Full implementation (Google + GitHub for v1, Apple + Discord
|
|
138
|
+
* deferred) lands in a follow-up release on the `next` dist-tag, before
|
|
139
|
+
* the `latest` 0.3.0 ships.
|
|
140
|
+
*
|
|
141
|
+
* Until then: drive your own OAuth flow and pass the resulting user
|
|
142
|
+
* identifier to `submitScore` directly.
|
|
143
|
+
*
|
|
144
|
+
* @since 0.3.0
|
|
145
|
+
* @stability preview
|
|
146
|
+
*/
|
|
147
|
+
declare function useAuthProvider(_options: {
|
|
148
|
+
readonly provider: 'google' | 'github';
|
|
149
|
+
}): never;
|
|
150
|
+
|
|
151
|
+
export { type AnonymousPlayerOptions, type PlayerHandle, type PromptedPlayerOptions, type ServerAuthoritativeMarker, useAnonymousPlayer, useAuthProvider, usePromptedPlayer, useServerAuthoritative };
|
package/dist/identity.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// src/identity.ts
|
|
2
|
+
var isBrowser = () => typeof window !== "undefined";
|
|
3
|
+
function readPersisted(key) {
|
|
4
|
+
if (!isBrowser()) return null;
|
|
5
|
+
try {
|
|
6
|
+
return window.localStorage.getItem(key);
|
|
7
|
+
} catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function writePersisted(key, value) {
|
|
12
|
+
if (!isBrowser()) return;
|
|
13
|
+
try {
|
|
14
|
+
window.localStorage.setItem(key, value);
|
|
15
|
+
} catch {
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function removePersisted(key) {
|
|
19
|
+
if (!isBrowser()) return;
|
|
20
|
+
try {
|
|
21
|
+
window.localStorage.removeItem(key);
|
|
22
|
+
} catch {
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function mintUuid() {
|
|
26
|
+
if (isBrowser() && typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
27
|
+
return crypto.randomUUID();
|
|
28
|
+
}
|
|
29
|
+
return `anon-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
30
|
+
}
|
|
31
|
+
function requireStorageKey(fnName, options) {
|
|
32
|
+
if (!options || typeof options.storageKey !== "string" || options.storageKey.length === 0) {
|
|
33
|
+
throw new TypeError(`${fnName}: options.storageKey is required (non-empty string)`);
|
|
34
|
+
}
|
|
35
|
+
return options.storageKey;
|
|
36
|
+
}
|
|
37
|
+
function useAnonymousPlayer(options) {
|
|
38
|
+
const storageKey = requireStorageKey("useAnonymousPlayer", options);
|
|
39
|
+
let id = readPersisted(storageKey);
|
|
40
|
+
if (id === null || id.length === 0) {
|
|
41
|
+
id = mintUuid();
|
|
42
|
+
writePersisted(storageKey, id);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
id,
|
|
46
|
+
forget: () => removePersisted(storageKey)
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function usePromptedPlayer(options) {
|
|
50
|
+
const storageKey = requireStorageKey("usePromptedPlayer", options);
|
|
51
|
+
if (typeof options.prompt !== "string" || options.prompt.length === 0) {
|
|
52
|
+
throw new TypeError("usePromptedPlayer: options.prompt is required (non-empty string)");
|
|
53
|
+
}
|
|
54
|
+
let id = readPersisted(storageKey);
|
|
55
|
+
if (id === null || id.length === 0) {
|
|
56
|
+
if (!isBrowser() || typeof window.prompt !== "function") {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const entered = window.prompt(options.prompt);
|
|
60
|
+
if (entered === null || entered.length === 0) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
id = entered;
|
|
64
|
+
writePersisted(storageKey, id);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
id,
|
|
68
|
+
forget: () => removePersisted(storageKey)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function useServerAuthoritative() {
|
|
72
|
+
return { source: "server-authoritative" };
|
|
73
|
+
}
|
|
74
|
+
function useAuthProvider(_options) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
"useAuthProvider is not yet implemented in this 0.3.0-next preview. OAuth provider helpers (Google + GitHub for v1) ship in a follow-up release on the `next` dist-tag. Until then, drive your own OAuth flow and pass the resulting user identifier to submitScore directly."
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export { useAnonymousPlayer, useAuthProvider, usePromptedPlayer, useServerAuthoritative };
|
|
81
|
+
//# sourceMappingURL=identity.js.map
|
|
82
|
+
//# sourceMappingURL=identity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/identity.ts"],"names":[],"mappings":"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","file":"identity.js","sourcesContent":["/**\n * Identity preset helpers — opinionated, selectable ways for your game to\n * generate or fetch a `playerId` for score submission.\n *\n * Background: every Scorezilla score carries an opaque `playerId`. The SDK\n * doesn't care whether it's a UUID, a nickname, an email, or a server\n * session token. But _how_ your game decides on that value is a UX +\n * privacy decision the team should make explicitly. These presets are the\n * blessed patterns; pick one per integration.\n *\n * See ADR 0003 (MCP identity axis) for the design rationale:\n * https://github.com/isco-tec/scorezilla/blob/main/docs/adr/0003-mcp-identity-axis.md\n *\n * @module scorezilla/identity\n * @since 0.3.0\n */\n\nexport interface AnonymousPlayerOptions {\n /** localStorage key under which the generated UUID is persisted. */\n readonly storageKey: string;\n}\n\nexport interface PromptedPlayerOptions {\n /** localStorage key under which the user-entered name is persisted. */\n readonly storageKey: string;\n /** Message shown in `window.prompt()` on first run. */\n readonly prompt: string;\n}\n\n/**\n * Identity handle returned by the storage-backed presets.\n *\n * `forget()` clears the persisted value from browser storage. It does\n * **not** delete server-side score history for this player — to fully\n * erase a player's data, call the admin \"delete player\" endpoint.\n */\nexport interface PlayerHandle {\n readonly id: string;\n readonly forget: () => void;\n}\n\n/**\n * Marker returned by `useServerAuthoritative()` to signal that the\n * game's backend (not the browser) owns the `playerId` via the\n * HMAC-signed secure path (`scorezilla/server`).\n */\nexport interface ServerAuthoritativeMarker {\n readonly source: 'server-authoritative';\n}\n\nconst isBrowser = (): boolean => typeof window !== 'undefined';\n\nfunction readPersisted(key: string): string | null {\n if (!isBrowser()) return null;\n try {\n return window.localStorage.getItem(key);\n } catch {\n // Storage may throw in sandboxed iframes, privacy mode, or when the\n // user has disabled site data. Treat as \"missing\"; the caller will\n // mint or re-prompt.\n return null;\n }\n}\n\nfunction writePersisted(key: string, value: string): void {\n if (!isBrowser()) return;\n try {\n window.localStorage.setItem(key, value);\n } catch {\n // ignore; next call will re-mint or re-prompt\n }\n}\n\nfunction removePersisted(key: string): void {\n if (!isBrowser()) return;\n try {\n window.localStorage.removeItem(key);\n } catch {\n // ignore\n }\n}\n\nfunction mintUuid(): string {\n if (isBrowser() && typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID();\n }\n // Best-effort fallback: timestamp + random suffix. Not cryptographically\n // strong, but opaque enough for the identifier-only use case. The\n // browsers we target (Chrome 92+, Firefox 95+, Safari 15.4+) all have\n // crypto.randomUUID — this branch is reached only in non-browser\n // environments where useAnonymousPlayer shouldn't be called anyway.\n return `anon-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction requireStorageKey(fnName: string, options: { storageKey?: unknown } | undefined): string {\n if (!options || typeof options.storageKey !== 'string' || options.storageKey.length === 0) {\n throw new TypeError(`${fnName}: options.storageKey is required (non-empty string)`);\n }\n return options.storageKey;\n}\n\n/**\n * Anonymous player identity. Generates an opaque UUID on first run and\n * persists it in `localStorage` so the same browser keeps the same ID\n * across page reloads.\n *\n * **Privacy.** Stores a randomly-generated UUID in browser localStorage;\n * the value is sent to the API on every score submission and persisted\n * indefinitely in the player's score-history rows. No PII is collected.\n * `forget()` removes the localStorage entry; for full server-side erasure\n * call the admin \"delete player\" endpoint.\n *\n * @example\n * ```ts\n * import { Scorezilla } from 'scorezilla';\n * import { useAnonymousPlayer } from 'scorezilla/identity';\n *\n * const player = useAnonymousPlayer({ storageKey: 'mygame:player' });\n * const sz = new Scorezilla({ publicKey: 'pk_…' });\n * await sz.submitScore({ boardId, playerId: player.id, score: 42 });\n * ```\n *\n * @since 0.3.0\n * @stability stable\n */\nexport function useAnonymousPlayer(options: AnonymousPlayerOptions): PlayerHandle {\n const storageKey = requireStorageKey('useAnonymousPlayer', options);\n let id = readPersisted(storageKey);\n if (id === null || id.length === 0) {\n id = mintUuid();\n writePersisted(storageKey, id);\n }\n return {\n id,\n forget: () => removePersisted(storageKey),\n };\n}\n\n/**\n * Prompted player identity. On first run shows a `window.prompt()` asking\n * the user for a name, then persists it in `localStorage` for subsequent\n * visits. Returns `null` if there is no browser (SSR), no `window.prompt`,\n * or if the user cancelled / entered an empty value.\n *\n * **Privacy.** The user-entered string is stored in browser localStorage,\n * transmitted to the API on every score submission, and persisted\n * indefinitely on the leaderboard. The persisted value is whatever the\n * user typed — sanitize at the UI layer if you care. `forget()` clears\n * local state but does NOT delete server-side history.\n *\n * **UX caveat.** `window.prompt()` blocks the main thread and looks\n * dated in modern apps. For a polished flow, build your own inline form\n * and pass the result to `submitScore` directly — the preset is here to\n * cover quick prototypes and jam-style integrations.\n *\n * @example\n * ```ts\n * import { Scorezilla } from 'scorezilla';\n * import { usePromptedPlayer } from 'scorezilla/identity';\n *\n * const player = usePromptedPlayer({\n * storageKey: 'mygame:player',\n * prompt: 'Enter a name for the leaderboard:',\n * });\n *\n * if (player) {\n * const sz = new Scorezilla({ publicKey: 'pk_…' });\n * await sz.submitScore({ boardId, playerId: player.id, score: 42 });\n * }\n * ```\n *\n * @since 0.3.0\n * @stability stable\n */\nexport function usePromptedPlayer(options: PromptedPlayerOptions): PlayerHandle | null {\n const storageKey = requireStorageKey('usePromptedPlayer', options);\n if (typeof options.prompt !== 'string' || options.prompt.length === 0) {\n throw new TypeError('usePromptedPlayer: options.prompt is required (non-empty string)');\n }\n\n let id = readPersisted(storageKey);\n if (id === null || id.length === 0) {\n if (!isBrowser() || typeof window.prompt !== 'function') {\n return null;\n }\n const entered = window.prompt(options.prompt);\n if (entered === null || entered.length === 0) {\n return null;\n }\n id = entered;\n writePersisted(storageKey, id);\n }\n return {\n id,\n forget: () => removePersisted(storageKey),\n };\n}\n\n/**\n * Server-authoritative identity marker. Signals that the game's backend\n * is responsible for the `playerId` via the HMAC-signed secure path\n * (`scorezilla/server`). The browser SDK does no identity work — the\n * server picks the value, signs the submission, and posts.\n *\n * The return value is a no-op marker; you don't pass it anywhere. It\n * exists so MCP-returned snippets can emit a single line that\n * unambiguously says \"this game uses the secure path; identity is\n * server-authoritative.\"\n *\n * @example\n * ```ts\n * // Client (no identity helper needed):\n * import { useServerAuthoritative } from 'scorezilla/identity';\n * useServerAuthoritative();\n *\n * // Server (where the real work happens):\n * import { Scorezilla } from 'scorezilla/server';\n * const sz = new Scorezilla({ secretKey: process.env.SCOREZILLA_SECRET_KEY! });\n * await sz.submitScore({ boardId, playerId: serverDerivedId, score });\n * ```\n *\n * @since 0.3.0\n * @stability stable\n */\nexport function useServerAuthoritative(): ServerAuthoritativeMarker {\n return { source: 'server-authoritative' };\n}\n\n/**\n * OAuth-backed player identity. **Preview stub in 0.3.0-next.x** — throws\n * on call. Full implementation (Google + GitHub for v1, Apple + Discord\n * deferred) lands in a follow-up release on the `next` dist-tag, before\n * the `latest` 0.3.0 ships.\n *\n * Until then: drive your own OAuth flow and pass the resulting user\n * identifier to `submitScore` directly.\n *\n * @since 0.3.0\n * @stability preview\n */\nexport function useAuthProvider(_options: { readonly provider: 'google' | 'github' }): never {\n throw new Error(\n 'useAuthProvider is not yet implemented in this 0.3.0-next preview. ' +\n 'OAuth provider helpers (Google + GitHub for v1) ship in a follow-up ' +\n 'release on the `next` dist-tag. Until then, drive your own OAuth flow ' +\n 'and pass the resulting user identifier to submitScore directly.',\n );\n}\n"]}
|
package/dist/index.cjs
CHANGED
|
@@ -33,6 +33,7 @@ function validateConfig(cfg) {
|
|
|
33
33
|
timeoutMs: cfg.timeoutMs,
|
|
34
34
|
maxRetries: cfg.maxRetries,
|
|
35
35
|
sleepImpl: cfg.sleepImpl,
|
|
36
|
+
warn: cfg.warn,
|
|
36
37
|
userAgent: cfg.userAgent,
|
|
37
38
|
auth
|
|
38
39
|
};
|
|
@@ -119,7 +120,9 @@ var ScorezillaError = class _ScorezillaError extends Error {
|
|
|
119
120
|
* {@link ScorezillaErrorCode}. For network errors, this is `'network_error'`;
|
|
120
121
|
* for aborts, `'aborted'`; for timeouts, `'timeout'`. */
|
|
121
122
|
code;
|
|
122
|
-
/** Sub-classifier — present on
|
|
123
|
+
/** Sub-classifier — present on:
|
|
124
|
+
* - `out_of_bounds`: `'below_min' | 'above_max'`
|
|
125
|
+
* - `usage_cap_exceeded`: `'over_cap' | 'suspended'`
|
|
123
126
|
* and possibly other codes in future minor releases. */
|
|
124
127
|
reason;
|
|
125
128
|
/** Seconds — present on `rate_limited`. Honored by the transport's retry
|
|
@@ -132,6 +135,20 @@ var ScorezillaError = class _ScorezillaError extends Error {
|
|
|
132
135
|
bound;
|
|
133
136
|
/** Which rate-limit layer fired on `rate_limited`. */
|
|
134
137
|
layer;
|
|
138
|
+
/** Tenant's billing tier — present on `usage_cap_exceeded`. */
|
|
139
|
+
tier;
|
|
140
|
+
/** The cap value crossed on `usage_cap_exceeded`. `0` indicates a
|
|
141
|
+
* suspended tenant. `undefined` on all other error codes. */
|
|
142
|
+
cap;
|
|
143
|
+
/** The post-increment submit count on `usage_cap_exceeded`. Always
|
|
144
|
+
* `> cap` when `reason === 'over_cap'`. */
|
|
145
|
+
count;
|
|
146
|
+
/** The period the count belongs to on `usage_cap_exceeded`, in `YYYY-MM`
|
|
147
|
+
* UTC form. */
|
|
148
|
+
period;
|
|
149
|
+
/** ISO-8601 timestamp of midnight UTC on the 1st of the next month —
|
|
150
|
+
* the counter's natural reset point on `usage_cap_exceeded`. */
|
|
151
|
+
resetsAt;
|
|
135
152
|
/** The underlying cause (e.g., a `TypeError: fetch failed`) for
|
|
136
153
|
* network/abort/timeout paths. `undefined` when the error came from a
|
|
137
154
|
* successfully-parsed API error body. */
|
|
@@ -146,6 +163,11 @@ var ScorezillaError = class _ScorezillaError extends Error {
|
|
|
146
163
|
this.requestId = truncateField(init.requestId);
|
|
147
164
|
this.bound = init.bound;
|
|
148
165
|
this.layer = truncateField(init.layer);
|
|
166
|
+
this.tier = truncateField(init.tier);
|
|
167
|
+
this.cap = init.cap;
|
|
168
|
+
this.count = init.count;
|
|
169
|
+
this.period = truncateField(init.period);
|
|
170
|
+
this.resetsAt = truncateField(init.resetsAt);
|
|
149
171
|
this.cause = init.cause;
|
|
150
172
|
Object.setPrototypeOf(this, _ScorezillaError.prototype);
|
|
151
173
|
if (typeof Error.captureStackTrace === "function") {
|
|
@@ -159,6 +181,22 @@ var ScorezillaError = class _ScorezillaError extends Error {
|
|
|
159
181
|
isRateLimited() {
|
|
160
182
|
return this.code === "rate_limited";
|
|
161
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* `true` when this error is a 402 / `usage_cap_exceeded`. The tenant
|
|
186
|
+
* has either hit their tier's monthly submit cap (`reason ===
|
|
187
|
+
* 'over_cap'`) or is suspended (`reason === 'suspended'`).
|
|
188
|
+
*
|
|
189
|
+
* Consumers SHOULD NOT auto-retry on this error — the cap doesn't lift
|
|
190
|
+
* until `resetsAt`. Surface to the developer with an upgrade prompt
|
|
191
|
+
* (over_cap) or contact-support message (suspended).
|
|
192
|
+
*/
|
|
193
|
+
isUsageCapExceeded() {
|
|
194
|
+
return this.code === "usage_cap_exceeded";
|
|
195
|
+
}
|
|
196
|
+
/** `true` when this error is a 402 + reason 'suspended' (vs over-cap). */
|
|
197
|
+
isSuspended() {
|
|
198
|
+
return this.code === "usage_cap_exceeded" && this.reason === "suspended";
|
|
199
|
+
}
|
|
162
200
|
/** `true` when this error is a 401 / `unauthorized` (or 403 / `forbidden`). */
|
|
163
201
|
isAuth() {
|
|
164
202
|
return this.code === "unauthorized" || this.code === "forbidden";
|
|
@@ -171,13 +209,21 @@ var ScorezillaError = class _ScorezillaError extends Error {
|
|
|
171
209
|
isOutOfBounds() {
|
|
172
210
|
return this.code === "out_of_bounds";
|
|
173
211
|
}
|
|
174
|
-
/** `true` for
|
|
175
|
-
*
|
|
212
|
+
/** `true` for the SDK's retryable conditions: pure network errors, 5xx, and
|
|
213
|
+
* 429. Deliberately excludes `timeout` and `aborted` — those are caller-
|
|
214
|
+
* observable terminal states, not transient. Aligned with `shouldRetryError`
|
|
215
|
+
* in `retry.ts` so a consumer mirroring the SDK's retry policy gets the
|
|
216
|
+
* same answer the transport does. */
|
|
176
217
|
isTransient() {
|
|
177
|
-
if (this.
|
|
218
|
+
if (this.code === "network_error") return true;
|
|
178
219
|
if (this.status >= 500 && this.status < 600) return true;
|
|
179
220
|
return this.isRateLimited();
|
|
180
221
|
}
|
|
222
|
+
/** `true` when this error is a 409 / `conflict` (idempotency-key conflict
|
|
223
|
+
* on retry). */
|
|
224
|
+
isConflict() {
|
|
225
|
+
return this.code === "conflict";
|
|
226
|
+
}
|
|
181
227
|
// ─── Factory ─────────────────────────────────────────────────────────
|
|
182
228
|
/**
|
|
183
229
|
* Build a `ScorezillaError` from a fetch round-trip outcome.
|
|
@@ -198,6 +244,13 @@ var ScorezillaError = class _ScorezillaError extends Error {
|
|
|
198
244
|
retryAfter: body.retryAfter,
|
|
199
245
|
bound: body.bound,
|
|
200
246
|
layer: body.layer,
|
|
247
|
+
// Usage-cap fields from `ApiError` (populated by the server on
|
|
248
|
+
// 402 responses; undefined on other errors).
|
|
249
|
+
tier: body.tier,
|
|
250
|
+
cap: body.cap,
|
|
251
|
+
count: body.count,
|
|
252
|
+
period: body.period,
|
|
253
|
+
resetsAt: body.resetsAt,
|
|
201
254
|
requestId,
|
|
202
255
|
cause
|
|
203
256
|
});
|
|
@@ -239,8 +292,10 @@ var ScorezillaError = class _ScorezillaError extends Error {
|
|
|
239
292
|
};
|
|
240
293
|
function codeForStatus(status) {
|
|
241
294
|
if (status === 401) return "unauthorized";
|
|
295
|
+
if (status === 402) return "usage_cap_exceeded";
|
|
242
296
|
if (status === 403) return "forbidden";
|
|
243
297
|
if (status === 404) return "not_found";
|
|
298
|
+
if (status === 409) return "conflict";
|
|
244
299
|
if (status === 422) return "out_of_bounds";
|
|
245
300
|
if (status === 429) return "rate_limited";
|
|
246
301
|
if (status >= 500) return "internal_error";
|
|
@@ -351,6 +406,7 @@ async function request(opts) {
|
|
|
351
406
|
}
|
|
352
407
|
const response = await fetchImpl(url, init);
|
|
353
408
|
if (response.ok) {
|
|
409
|
+
warnOnDeprecationOnce(response, opts.warnImpl);
|
|
354
410
|
return await parseJson(response);
|
|
355
411
|
}
|
|
356
412
|
const body = await safelyParseErrorBody(response);
|
|
@@ -462,6 +518,29 @@ function readRetryAfter(response) {
|
|
|
462
518
|
const n = Number(raw);
|
|
463
519
|
return Number.isFinite(n) && n >= 0 ? n : void 0;
|
|
464
520
|
}
|
|
521
|
+
var seenDeprecations = /* @__PURE__ */ new Set();
|
|
522
|
+
function warnOnDeprecationOnce(response, warnImpl) {
|
|
523
|
+
const deprecation = response.headers.get("Deprecation");
|
|
524
|
+
const sunset = response.headers.get("Sunset");
|
|
525
|
+
if (!deprecation && !sunset) return;
|
|
526
|
+
const link = response.headers.get("Link") ?? "";
|
|
527
|
+
const key = `${deprecation ?? ""}|${sunset ?? ""}|${link}`;
|
|
528
|
+
if (seenDeprecations.has(key)) return;
|
|
529
|
+
seenDeprecations.add(key);
|
|
530
|
+
const detail = [];
|
|
531
|
+
if (deprecation === "true" || deprecation) detail.push(`Deprecation: ${deprecation}`);
|
|
532
|
+
if (sunset) detail.push(`Sunset: ${sunset}`);
|
|
533
|
+
if (link) {
|
|
534
|
+
const m = link.match(/<([^>]+)>/);
|
|
535
|
+
if (m) detail.push(`Docs: ${m[1]}`);
|
|
536
|
+
}
|
|
537
|
+
const message = `[scorezilla-sdk] API responded with deprecation signal: ${detail.join(" \xB7 ")}. Upgrade your SDK before the sunset date.`;
|
|
538
|
+
if (warnImpl) {
|
|
539
|
+
warnImpl(message);
|
|
540
|
+
} else {
|
|
541
|
+
console.warn(message);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
465
544
|
function combineSignalsWithTimeout(caller, timeoutMs) {
|
|
466
545
|
const ctrl = new AbortController();
|
|
467
546
|
let didTimeOut = false;
|
|
@@ -550,10 +629,11 @@ function validateMetadata(metadata) {
|
|
|
550
629
|
`scorezilla: metadata exceeds ${METADATA_MAX_BYTES} bytes (got ${byteLength} bytes when JSON-stringified)`
|
|
551
630
|
);
|
|
552
631
|
}
|
|
632
|
+
return serialized;
|
|
553
633
|
}
|
|
554
634
|
var Scorezilla = class _Scorezilla {
|
|
555
635
|
/** The package version, injected at build time from `package.json`. */
|
|
556
|
-
static version = "0.
|
|
636
|
+
static version = "0.3.0-next.0";
|
|
557
637
|
#config;
|
|
558
638
|
#userAgent;
|
|
559
639
|
#authHeader;
|
|
@@ -611,7 +691,8 @@ var Scorezilla = class _Scorezilla {
|
|
|
611
691
|
return this.#request({
|
|
612
692
|
path: submitScorePath(input.boardId),
|
|
613
693
|
method: "POST",
|
|
614
|
-
body
|
|
694
|
+
body,
|
|
695
|
+
signal: input.signal
|
|
615
696
|
});
|
|
616
697
|
}
|
|
617
698
|
/**
|
|
@@ -635,7 +716,8 @@ var Scorezilla = class _Scorezilla {
|
|
|
635
716
|
if (input.offset !== void 0) q.offset = input.offset;
|
|
636
717
|
return this.#request({
|
|
637
718
|
path: getLeaderboardPath(input.boardId, q),
|
|
638
|
-
method: "GET"
|
|
719
|
+
method: "GET",
|
|
720
|
+
signal: input.signal
|
|
639
721
|
});
|
|
640
722
|
}
|
|
641
723
|
/**
|
|
@@ -664,7 +746,8 @@ var Scorezilla = class _Scorezilla {
|
|
|
664
746
|
async getPlayerRank(input) {
|
|
665
747
|
return this.#request({
|
|
666
748
|
path: getPlayerRankPath(input.boardId, input.playerId),
|
|
667
|
-
method: "GET"
|
|
749
|
+
method: "GET",
|
|
750
|
+
signal: input.signal
|
|
668
751
|
});
|
|
669
752
|
}
|
|
670
753
|
/**
|
|
@@ -689,7 +772,8 @@ var Scorezilla = class _Scorezilla {
|
|
|
689
772
|
if (input.after !== void 0) q.after = input.after;
|
|
690
773
|
return this.#request({
|
|
691
774
|
path: getWindowAroundPath(input.boardId, input.playerId, q),
|
|
692
|
-
method: "GET"
|
|
775
|
+
method: "GET",
|
|
776
|
+
signal: input.signal
|
|
693
777
|
});
|
|
694
778
|
}
|
|
695
779
|
// ─── Internal ────────────────────────────────────────────────────────
|
|
@@ -716,7 +800,9 @@ var Scorezilla = class _Scorezilla {
|
|
|
716
800
|
headers
|
|
717
801
|
};
|
|
718
802
|
if (opts.body !== void 0) requestOpts.body = opts.body;
|
|
803
|
+
if (opts.signal !== void 0) requestOpts.signal = opts.signal;
|
|
719
804
|
if (this.#config.fetch !== void 0) requestOpts.fetchImpl = this.#config.fetch;
|
|
805
|
+
if (this.#config.warn !== void 0) requestOpts.warnImpl = this.#config.warn;
|
|
720
806
|
if (this.#config.timeoutMs !== void 0) requestOpts.timeoutMs = this.#config.timeoutMs;
|
|
721
807
|
if (this.#config.maxRetries !== void 0 || this.#config.sleepImpl !== void 0) {
|
|
722
808
|
requestOpts.retry = {
|
|
@@ -732,7 +818,7 @@ function createClient(config) {
|
|
|
732
818
|
}
|
|
733
819
|
|
|
734
820
|
// src/index.ts
|
|
735
|
-
var SDK_VERSION = "0.
|
|
821
|
+
var SDK_VERSION = "0.3.0-next.0";
|
|
736
822
|
|
|
737
823
|
exports.SDK_VERSION = SDK_VERSION;
|
|
738
824
|
exports.Scorezilla = Scorezilla;
|