@unboxy/phaser-sdk 0.2.0 → 0.2.3

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/SDK-GUIDE.md ADDED
@@ -0,0 +1,141 @@
1
+ # @unboxy/phaser-sdk — Agent Guide
2
+
3
+ Reference for AI agents building games on the Unboxy platform. Tracks the **installed SDK version** — always matches the code in `node_modules/@unboxy/phaser-sdk`. Read this before calling any platform API.
4
+
5
+ ## What the SDK provides
6
+
7
+ - `createUnboxyGame(options)` — Phaser.Game factory with platform defaults (screenshot + recording wired up)
8
+ - `UnboxyScene` — optional base scene with `createButton`, `shakeCamera`, `flashCamera` helpers
9
+ - `Unboxy.init()` — platform services: identity, save data
10
+ - (future: leaderboards, multiplayer lobbies)
11
+
12
+ ## Platform services
13
+
14
+ Initialize once at module load — resolves the host (home-ui, standalone, etc.) and returns a bound instance. Scenes read from the resulting promise.
15
+
16
+ ```ts
17
+ // src/main.ts
18
+ import { Unboxy } from '@unboxy/phaser-sdk';
19
+
20
+ export const unboxyReady = Unboxy.init({ standaloneGameId: 'my-game' })
21
+ .catch(() => null);
22
+ ```
23
+
24
+ `unboxyReady` resolves to an `Unboxy` instance, or `null` if init failed. Scenes should be resilient to `null` — they must still run for anonymous players.
25
+
26
+ ### Identity
27
+
28
+ ```ts
29
+ const unboxy = await unboxyReady;
30
+ if (unboxy) {
31
+ unboxy.user; // { id, name, avatar? } | null — null = anonymous
32
+ unboxy.isAuthenticated; // boolean
33
+ unboxy.gameId; // stable platform-issued game id
34
+ unboxy.host; // 'unboxy-home-ui' | 'standalone' | 'discord' (future)
35
+ }
36
+ ```
37
+
38
+ ### Save data — `unboxy.saves`
39
+
40
+ Per-user key-value store scoped to `(gameId, userId)`. Backed by the Unboxy backend when authenticated, by localStorage when standalone/anonymous — **the API is identical either way**. Games should never branch on auth state for save logic.
41
+
42
+ ```ts
43
+ // Read — returns null if the key doesn't exist
44
+ const highScore = await unboxy.saves.get<number>('highScore');
45
+
46
+ // Write — returns the new version number
47
+ await unboxy.saves.set('highScore', 12340);
48
+
49
+ // Optimistic concurrency — write only if stored version matches
50
+ try {
51
+ await unboxy.saves.set('progress', { level: 3 }, { ifVersion: 7 });
52
+ } catch (err) {
53
+ // RpcError with code 'VERSION_MISMATCH' — someone else wrote first
54
+ }
55
+
56
+ // List keys (no values — cheap)
57
+ const keys = await unboxy.saves.list();
58
+
59
+ // Delete
60
+ await unboxy.saves.delete('progress');
61
+ ```
62
+
63
+ **Type parameter on `.get<T>()`** is only for TypeScript convenience — the SDK does not validate. Treat returned data as untrusted and defend against missing/malformed values.
64
+
65
+ ### Quotas (enforced at backend)
66
+
67
+ | Limit | Value |
68
+ |---|---|
69
+ | Per value | 100 KB |
70
+ | Per `(game, user)` total | 1 MB |
71
+ | Max keys per `(game, user)` | 64 |
72
+ | Key format | `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$` |
73
+
74
+ Exceeding a quota throws `RpcError` with code `QUOTA_EXCEEDED`. Don't catch silently — surface a clear error.
75
+
76
+ ### When to use saves
77
+
78
+ **Persist (by convention, no need to ask):**
79
+ - High scores, personal bests
80
+ - Level / stage / chapter progression
81
+ - Unlockables (characters, skins, abilities)
82
+ - In-game currency, inventories
83
+ - Cosmetic selection (chosen character, color)
84
+ - User-configurable settings (volume, difficulty preference, control scheme)
85
+ - Tutorial-seen / onboarding-completed flags
86
+
87
+ **Do not persist (transient state):**
88
+ - Current-run position, velocity, HP
89
+ - Active enemies, projectiles, particles
90
+ - Scene camera position
91
+ - Animation frames, timers, cooldowns
92
+ - UI focus, current menu
93
+
94
+ **Rule of thumb:** if the user would be annoyed to lose it on refresh, persist. If they'd be confused to see it come back (e.g. "why is there an enemy here from my last game?"), don't.
95
+
96
+ ### Suggested key names
97
+
98
+ Use stable, descriptive key names so data carries across SDK versions:
99
+
100
+ | Concept | Key |
101
+ |---|---|
102
+ | Highest score ever | `highScore` |
103
+ | Current progression | `progress` |
104
+ | Unlocked items | `unlocks` |
105
+ | Player settings | `settings` |
106
+ | Cosmetic selection | `cosmetics` |
107
+ | Onboarding flag | `onboardedAt` |
108
+
109
+ ### Error handling patterns
110
+
111
+ ```ts
112
+ // Graceful degradation — save failures must never block gameplay
113
+ try {
114
+ await unboxy.saves.set('highScore', score);
115
+ } catch (err) {
116
+ console.warn('[game] failed to save highScore', err);
117
+ // continue — game still works, just without persistence this turn
118
+ }
119
+ ```
120
+
121
+ ```ts
122
+ // Load at scene start — handle missing, malformed, and failure cases
123
+ const saved = await unboxy.saves.get<number>('highScore').catch(() => null);
124
+ this.highScore = typeof saved === 'number' ? saved : 0;
125
+ ```
126
+
127
+ ## Anti-patterns (don't do these)
128
+
129
+ - Do **not** call `Unboxy.init()` inside a scene. Initialize at module load in `main.ts` and export the promise.
130
+ - Do **not** block scene creation on `unboxyReady`. Games must cold-start immediately; hydrate when the promise resolves.
131
+ - Do **not** save on every frame / every score tick. Debounce or only save on meaningful events (game over, level clear).
132
+ - Do **not** store large binary blobs (images, audio) as save values — use a dedicated blob API when it ships.
133
+ - Do **not** use `localStorage` directly for persistent data when `unboxy.saves` is available — you'll lose cross-device sync when we ship provisional accounts.
134
+
135
+ ## Changelog
136
+
137
+ - **0.2.3** — anonymous users now use localStorage even inside a host (was throwing UNAUTHENTICATED when calling `saves` in home-ui without login)
138
+ - **0.2.2** — added `SDK-GUIDE.md`
139
+ - **0.2.1** — added `Unboxy.init`, `unboxy.user`, `unboxy.saves`, Transport abstraction (PostMessage + LocalStorage backends)
140
+ - **0.2.0** — added `UnboxyScene`, recording support. Migrated to public npm.
141
+ - **0.1.0** — initial release (CodeArtifact).
@@ -0,0 +1,22 @@
1
+ import type { UnboxyUser } from '../protocol.js';
2
+ /**
3
+ * Low-level transport for platform RPC calls. Swapped per host.
4
+ *
5
+ * Current implementations:
6
+ * - PostMessageTransport: iframe hosted inside unboxy-home-ui
7
+ * - LocalStorageTransport: game running standalone (no host, no user)
8
+ *
9
+ * Future implementations (not shipped):
10
+ * - DiscordTransport: game running as a Discord Activity
11
+ */
12
+ export interface Transport {
13
+ readonly kind: TransportKind;
14
+ readonly user: UnboxyUser | null;
15
+ readonly gameId: string;
16
+ call<T = unknown>(method: string, params?: unknown): Promise<T>;
17
+ }
18
+ export type TransportKind = 'unboxy-home-ui' | 'standalone' | 'discord';
19
+ export declare class RpcError extends Error {
20
+ code: string;
21
+ constructor(code: string, message: string);
22
+ }
@@ -0,0 +1,7 @@
1
+ export class RpcError extends Error {
2
+ constructor(code, message) {
3
+ super(message);
4
+ this.code = code;
5
+ this.name = 'RpcError';
6
+ }
7
+ }
@@ -0,0 +1,41 @@
1
+ import type { TransportKind } from './Transport.js';
2
+ import { SavesModule } from '../saves/SavesModule.js';
3
+ import type { UnboxyUser } from '../protocol.js';
4
+ export interface UnboxyInitOptions {
5
+ /** Handshake timeout in ms when running inside a host. Default 2000. */
6
+ handshakeTimeoutMs?: number;
7
+ /**
8
+ * Game ID used for the localStorage fallback. Ignored when connected to a
9
+ * host (the host provides the authoritative gameId). Default 'standalone'.
10
+ */
11
+ standaloneGameId?: string;
12
+ }
13
+ /**
14
+ * Unboxy platform services bound to the current (game, user).
15
+ *
16
+ * The public API is host-agnostic. Under the hood, `Unboxy.init()` detects the
17
+ * host (home-ui iframe, Discord Activity, or standalone) and picks the right
18
+ * transport. Games should not care which one is active.
19
+ */
20
+ export declare class Unboxy {
21
+ private transport;
22
+ readonly saves: SavesModule;
23
+ private constructor();
24
+ /** The current authenticated user, or `null` if anonymous / standalone. */
25
+ get user(): UnboxyUser | null;
26
+ get isAuthenticated(): boolean;
27
+ /** Stable game identifier issued by the host. May be a placeholder when standalone. */
28
+ get gameId(): string;
29
+ /** Which transport Unboxy.init() selected. Useful for debugging. */
30
+ get host(): TransportKind;
31
+ /**
32
+ * Initialize the SDK. Detects the host and negotiates identity.
33
+ *
34
+ * Resolution order:
35
+ * 1. If embedded in a window that responds to the Unboxy handshake → PostMessageTransport
36
+ * 2. Otherwise → LocalStorageTransport (anonymous, local-only saves)
37
+ *
38
+ * Discord Activity support is designed for but not shipped in this version.
39
+ */
40
+ static init(options?: UnboxyInitOptions): Promise<Unboxy>;
41
+ }
@@ -0,0 +1,49 @@
1
+ import { PostMessageTransport } from './transports/PostMessageTransport.js';
2
+ import { LocalStorageTransport } from './transports/LocalStorageTransport.js';
3
+ import { SavesModule } from '../saves/SavesModule.js';
4
+ // Kept in sync with package.json on each publish.
5
+ const SDK_VERSION = '0.2.3';
6
+ /**
7
+ * Unboxy platform services bound to the current (game, user).
8
+ *
9
+ * The public API is host-agnostic. Under the hood, `Unboxy.init()` detects the
10
+ * host (home-ui iframe, Discord Activity, or standalone) and picks the right
11
+ * transport. Games should not care which one is active.
12
+ */
13
+ export class Unboxy {
14
+ constructor(transport) {
15
+ this.transport = transport;
16
+ // Saves are per-user. When the viewer is anonymous (no logged-in user),
17
+ // route save calls to localStorage even if a host is attached — the host
18
+ // backend requires auth, so forwarding would 401. Other module surfaces
19
+ // (future leaderboards, etc.) can still use the primary transport.
20
+ const savesTransport = transport.user === null
21
+ ? new LocalStorageTransport(transport.gameId || 'anonymous')
22
+ : transport;
23
+ this.saves = new SavesModule(savesTransport);
24
+ }
25
+ /** The current authenticated user, or `null` if anonymous / standalone. */
26
+ get user() { return this.transport.user; }
27
+ get isAuthenticated() { return this.transport.user !== null; }
28
+ /** Stable game identifier issued by the host. May be a placeholder when standalone. */
29
+ get gameId() { return this.transport.gameId; }
30
+ /** Which transport Unboxy.init() selected. Useful for debugging. */
31
+ get host() { return this.transport.kind; }
32
+ /**
33
+ * Initialize the SDK. Detects the host and negotiates identity.
34
+ *
35
+ * Resolution order:
36
+ * 1. If embedded in a window that responds to the Unboxy handshake → PostMessageTransport
37
+ * 2. Otherwise → LocalStorageTransport (anonymous, local-only saves)
38
+ *
39
+ * Discord Activity support is designed for but not shipped in this version.
40
+ */
41
+ static async init(options = {}) {
42
+ const handshakeTimeoutMs = options.handshakeTimeoutMs ?? 2000;
43
+ const standaloneGameId = options.standaloneGameId ?? 'standalone';
44
+ const pm = await PostMessageTransport.connect(handshakeTimeoutMs, SDK_VERSION);
45
+ if (pm)
46
+ return new Unboxy(pm);
47
+ return new Unboxy(new LocalStorageTransport(standaloneGameId));
48
+ }
49
+ }
@@ -0,0 +1,21 @@
1
+ import type { Transport } from '../Transport.js';
2
+ import type { UnboxyUser } from '../../protocol.js';
3
+ /**
4
+ * Fallback transport when the game runs without a host (e.g., opened directly
5
+ * outside unboxy-home-ui) or when the viewer is anonymous. Data lives in
6
+ * localStorage, keyed by gameId. No network calls. Quotas mirror the backend.
7
+ */
8
+ export declare class LocalStorageTransport implements Transport {
9
+ readonly gameId: string;
10
+ readonly kind: "standalone";
11
+ readonly user: UnboxyUser | null;
12
+ constructor(gameId: string);
13
+ call<T = unknown>(method: string, params?: unknown): Promise<T>;
14
+ private prefix;
15
+ private read;
16
+ private write;
17
+ private savesGet;
18
+ private savesSet;
19
+ private savesDelete;
20
+ private savesList;
21
+ }
@@ -0,0 +1,74 @@
1
+ import { RpcError } from '../Transport.js';
2
+ /**
3
+ * Fallback transport when the game runs without a host (e.g., opened directly
4
+ * outside unboxy-home-ui) or when the viewer is anonymous. Data lives in
5
+ * localStorage, keyed by gameId. No network calls. Quotas mirror the backend.
6
+ */
7
+ export class LocalStorageTransport {
8
+ constructor(gameId) {
9
+ this.gameId = gameId;
10
+ this.kind = 'standalone';
11
+ this.user = null;
12
+ }
13
+ async call(method, params) {
14
+ switch (method) {
15
+ case 'saves.get': return this.savesGet(params);
16
+ case 'saves.set': return this.savesSet(params);
17
+ case 'saves.delete': return this.savesDelete(params);
18
+ case 'saves.list': return this.savesList();
19
+ default: throw new RpcError('UNKNOWN_METHOD', `Unknown method: ${method}`);
20
+ }
21
+ }
22
+ prefix() { return `unboxy:saves:${this.gameId}:`; }
23
+ read(key) {
24
+ if (typeof localStorage === 'undefined')
25
+ return null;
26
+ const raw = localStorage.getItem(this.prefix() + key);
27
+ if (!raw)
28
+ return null;
29
+ try {
30
+ return JSON.parse(raw);
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ write(key, rec) {
37
+ if (typeof localStorage === 'undefined')
38
+ throw new RpcError('UNAVAILABLE', 'localStorage unavailable');
39
+ localStorage.setItem(this.prefix() + key, JSON.stringify(rec));
40
+ }
41
+ savesGet({ key }) {
42
+ const rec = this.read(key);
43
+ return rec ? { value: rec.value, version: rec.version } : { value: null, version: null };
44
+ }
45
+ savesSet({ key, value, ifVersion }) {
46
+ const existing = this.read(key);
47
+ if (ifVersion !== undefined && existing && existing.version !== ifVersion) {
48
+ throw new RpcError('VERSION_MISMATCH', `Expected version ${ifVersion} but stored is ${existing.version}`);
49
+ }
50
+ const next = { value, version: (existing?.version ?? 0) + 1 };
51
+ this.write(key, next);
52
+ return { version: next.version };
53
+ }
54
+ savesDelete({ key }) {
55
+ if (typeof localStorage === 'undefined')
56
+ return { deleted: false };
57
+ const full = this.prefix() + key;
58
+ const existed = localStorage.getItem(full) !== null;
59
+ localStorage.removeItem(full);
60
+ return { deleted: existed };
61
+ }
62
+ savesList() {
63
+ if (typeof localStorage === 'undefined')
64
+ return { keys: [] };
65
+ const keys = [];
66
+ const p = this.prefix();
67
+ for (let i = 0; i < localStorage.length; i++) {
68
+ const k = localStorage.key(i);
69
+ if (k && k.startsWith(p))
70
+ keys.push(k.slice(p.length));
71
+ }
72
+ return { keys };
73
+ }
74
+ }
@@ -0,0 +1,22 @@
1
+ import type { Transport } from '../Transport.js';
2
+ import { type UnboxyUser } from '../../protocol.js';
3
+ /**
4
+ * Connects to a parent window that implements the Unboxy RPC host protocol.
5
+ * Used when the game is loaded inside unboxy-home-ui.
6
+ */
7
+ export declare class PostMessageTransport implements Transport {
8
+ private parent;
9
+ readonly kind: "unboxy-home-ui";
10
+ user: UnboxyUser | null;
11
+ gameId: string;
12
+ private pending;
13
+ private seq;
14
+ private constructor();
15
+ /**
16
+ * Perform the handshake. Resolves once parent has replied with `unboxy:init`.
17
+ * Rejects if no response within `timeoutMs`.
18
+ */
19
+ static connect(timeoutMs?: number, sdkVersion?: string): Promise<PostMessageTransport | null>;
20
+ private installResultListener;
21
+ call<T = unknown>(method: string, params?: unknown): Promise<T>;
22
+ }
@@ -0,0 +1,88 @@
1
+ import { RpcError } from '../Transport.js';
2
+ import { PROTOCOL_VERSION, } from '../../protocol.js';
3
+ /**
4
+ * Connects to a parent window that implements the Unboxy RPC host protocol.
5
+ * Used when the game is loaded inside unboxy-home-ui.
6
+ */
7
+ export class PostMessageTransport {
8
+ constructor(parent) {
9
+ this.parent = parent;
10
+ this.kind = 'unboxy-home-ui';
11
+ this.user = null;
12
+ this.gameId = '';
13
+ this.pending = new Map();
14
+ this.seq = 0;
15
+ }
16
+ /**
17
+ * Perform the handshake. Resolves once parent has replied with `unboxy:init`.
18
+ * Rejects if no response within `timeoutMs`.
19
+ */
20
+ static async connect(timeoutMs = 2000, sdkVersion = '0.0.0') {
21
+ if (typeof window === 'undefined' || window.parent === window)
22
+ return null;
23
+ const transport = new PostMessageTransport(window.parent);
24
+ const initPromise = new Promise((resolve) => {
25
+ const onMessage = (event) => {
26
+ if (event.source !== window.parent)
27
+ return;
28
+ const data = event.data;
29
+ if (!data || data.type !== 'unboxy:init')
30
+ return;
31
+ window.removeEventListener('message', onMessage);
32
+ resolve(data);
33
+ };
34
+ window.addEventListener('message', onMessage);
35
+ setTimeout(() => {
36
+ window.removeEventListener('message', onMessage);
37
+ resolve(null);
38
+ }, timeoutMs);
39
+ });
40
+ const hello = {
41
+ type: 'unboxy:hello',
42
+ protocolVersion: PROTOCOL_VERSION,
43
+ sdkVersion,
44
+ };
45
+ transport.parent.postMessage(hello, '*');
46
+ const init = await initPromise;
47
+ if (!init)
48
+ return null;
49
+ if (init.protocolVersion !== PROTOCOL_VERSION) {
50
+ console.warn('[UnboxySDK] protocol version mismatch', init.protocolVersion, PROTOCOL_VERSION);
51
+ }
52
+ transport.user = init.user;
53
+ transport.gameId = init.gameId;
54
+ transport.installResultListener();
55
+ return transport;
56
+ }
57
+ installResultListener() {
58
+ window.addEventListener('message', (event) => {
59
+ if (event.source !== this.parent)
60
+ return;
61
+ const data = event.data;
62
+ if (!data || data.type !== 'unboxy:rpc.result')
63
+ return;
64
+ const pending = this.pending.get(data.id);
65
+ if (!pending)
66
+ return;
67
+ this.pending.delete(data.id);
68
+ if (data.ok)
69
+ pending.resolve(data.result);
70
+ else
71
+ pending.reject(new RpcError(data.error.code, data.error.message));
72
+ });
73
+ }
74
+ call(method, params) {
75
+ const id = `${++this.seq}-${Date.now()}`;
76
+ const message = { type: 'unboxy:rpc', id, method, params };
77
+ return new Promise((resolve, reject) => {
78
+ this.pending.set(id, { resolve: resolve, reject });
79
+ this.parent.postMessage(message, '*');
80
+ setTimeout(() => {
81
+ if (this.pending.has(id)) {
82
+ this.pending.delete(id);
83
+ reject(new RpcError('TIMEOUT', `RPC ${method} timed out`));
84
+ }
85
+ }, 10000);
86
+ });
87
+ }
88
+ }
package/dist/index.d.ts CHANGED
@@ -3,3 +3,10 @@ export type { UnboxyGameOptions } from './core/UnboxyGame.js';
3
3
  export { UnboxyScene } from './core/UnboxyScene.js';
4
4
  export { setupScreenshotListener, takeScreenshot } from './screenshot/ScreenshotManager.js';
5
5
  export { setupRecordingListener } from './recording/RecordingManager.js';
6
+ export { Unboxy } from './core/Unboxy.js';
7
+ export type { UnboxyInitOptions } from './core/Unboxy.js';
8
+ export { SavesModule } from './saves/SavesModule.js';
9
+ export { RpcError } from './core/Transport.js';
10
+ export type { Transport, TransportKind } from './core/Transport.js';
11
+ export type { UnboxyUser } from './protocol.js';
12
+ export { PROTOCOL_VERSION, type HelloMessage, type InitMessage, type RpcRequestMessage, type RpcResultOk, type RpcResultError, type HostToSdkMessage, type SdkToHostMessage, type RpcErrorPayload, type RpcMethod, type SavesGetParams, type SavesGetResult, type SavesSetParams, type SavesSetResult, type SavesDeleteParams, type SavesDeleteResult, type SavesListResult, } from './protocol.js';
package/dist/index.js CHANGED
@@ -5,3 +5,8 @@ export { UnboxyScene } from './core/UnboxyScene.js';
5
5
  export { setupScreenshotListener, takeScreenshot } from './screenshot/ScreenshotManager.js';
6
6
  // Recording
7
7
  export { setupRecordingListener } from './recording/RecordingManager.js';
8
+ // Platform services (v0.2.1+)
9
+ export { Unboxy } from './core/Unboxy.js';
10
+ export { SavesModule } from './saves/SavesModule.js';
11
+ export { RpcError } from './core/Transport.js';
12
+ export { PROTOCOL_VERSION, } from './protocol.js';
@@ -0,0 +1,67 @@
1
+ export declare const PROTOCOL_VERSION = 1;
2
+ export interface UnboxyUser {
3
+ id: string;
4
+ name: string;
5
+ avatar?: string;
6
+ }
7
+ export interface HelloMessage {
8
+ type: 'unboxy:hello';
9
+ protocolVersion: number;
10
+ sdkVersion: string;
11
+ }
12
+ export interface RpcRequestMessage {
13
+ type: 'unboxy:rpc';
14
+ id: string;
15
+ method: string;
16
+ params?: unknown;
17
+ }
18
+ export type SdkToHostMessage = HelloMessage | RpcRequestMessage;
19
+ export interface InitMessage {
20
+ type: 'unboxy:init';
21
+ protocolVersion: number;
22
+ gameId: string;
23
+ user: UnboxyUser | null;
24
+ capabilities: string[];
25
+ }
26
+ export interface RpcResultOk {
27
+ type: 'unboxy:rpc.result';
28
+ id: string;
29
+ ok: true;
30
+ result: unknown;
31
+ }
32
+ export interface RpcErrorPayload {
33
+ code: string;
34
+ message: string;
35
+ }
36
+ export interface RpcResultError {
37
+ type: 'unboxy:rpc.result';
38
+ id: string;
39
+ ok: false;
40
+ error: RpcErrorPayload;
41
+ }
42
+ export type HostToSdkMessage = InitMessage | RpcResultOk | RpcResultError;
43
+ export type RpcMethod = 'saves.get' | 'saves.set' | 'saves.delete' | 'saves.list';
44
+ export interface SavesGetParams {
45
+ key: string;
46
+ }
47
+ export interface SavesGetResult {
48
+ value: unknown | null;
49
+ version: number | null;
50
+ }
51
+ export interface SavesSetParams {
52
+ key: string;
53
+ value: unknown;
54
+ ifVersion?: number;
55
+ }
56
+ export interface SavesSetResult {
57
+ version: number;
58
+ }
59
+ export interface SavesDeleteParams {
60
+ key: string;
61
+ }
62
+ export type SavesDeleteResult = {
63
+ deleted: boolean;
64
+ };
65
+ export interface SavesListResult {
66
+ keys: string[];
67
+ }
@@ -0,0 +1,3 @@
1
+ // Shared postMessage protocol between the game SDK (iframe) and its host (unboxy-home-ui).
2
+ // Kept as a single file so host and SDK can be type-checked against the same contract.
3
+ export const PROTOCOL_VERSION = 1;
@@ -0,0 +1,23 @@
1
+ import type { Transport } from '../core/Transport.js';
2
+ /**
3
+ * Per-user key-value save data scoped to the current (game, user).
4
+ *
5
+ * When the viewer is authenticated, reads/writes go through the host to the
6
+ * Unboxy backend. When anonymous or standalone, the same API is backed by
7
+ * localStorage — games do not branch on auth state.
8
+ *
9
+ * Size quotas (enforced at the backend):
10
+ * - 100 KB per value
11
+ * - 1 MB total per (game, user)
12
+ * - 64 keys per (game, user)
13
+ */
14
+ export declare class SavesModule {
15
+ private transport;
16
+ constructor(transport: Transport);
17
+ get<T = unknown>(key: string): Promise<T | null>;
18
+ set(key: string, value: unknown, options?: {
19
+ ifVersion?: number;
20
+ }): Promise<number>;
21
+ delete(key: string): Promise<boolean>;
22
+ list(): Promise<string[]>;
23
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Per-user key-value save data scoped to the current (game, user).
3
+ *
4
+ * When the viewer is authenticated, reads/writes go through the host to the
5
+ * Unboxy backend. When anonymous or standalone, the same API is backed by
6
+ * localStorage — games do not branch on auth state.
7
+ *
8
+ * Size quotas (enforced at the backend):
9
+ * - 100 KB per value
10
+ * - 1 MB total per (game, user)
11
+ * - 64 keys per (game, user)
12
+ */
13
+ export class SavesModule {
14
+ constructor(transport) {
15
+ this.transport = transport;
16
+ }
17
+ async get(key) {
18
+ const res = await this.transport.call('saves.get', { key });
19
+ return (res?.value ?? null);
20
+ }
21
+ async set(key, value, options) {
22
+ const res = await this.transport.call('saves.set', {
23
+ key,
24
+ value,
25
+ ifVersion: options?.ifVersion,
26
+ });
27
+ return res.version;
28
+ }
29
+ async delete(key) {
30
+ const res = await this.transport.call('saves.delete', { key });
31
+ return res.deleted;
32
+ }
33
+ async list() {
34
+ const res = await this.transport.call('saves.list');
35
+ return res.keys;
36
+ }
37
+ }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@unboxy/phaser-sdk",
3
- "version": "0.2.0",
3
+ "version": "0.2.3",
4
4
  "description": "Unboxy Phaser 3 SDK — game infrastructure for the Unboxy platform",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
- "files": ["dist"],
7
+ "files": ["dist", "SDK-GUIDE.md"],
8
8
  "scripts": {
9
9
  "build": "tsc",
10
10
  "prepublishOnly": "npm run build"