@fzgame/sdk 0.0.0-stage → 0.3.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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 Fanzeal
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,249 @@
1
- # Temporary Holding Version
1
+ # @fzgame/sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Typed client for the GameHub server API. The types come from the server's own OpenAPI document, so
4
+ every path, parameter, and response you touch is checked at compile time.
5
+
6
+ The client is `openapi-fetch` underneath and has no other runtime dependency. The root entry works in
7
+ Node and in the browser; the optional `@fzgame/sdk/react` entry adds a React component for apps that
8
+ want the client configured from a component tree.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pnpm add @fzgame/sdk
14
+ ```
15
+
16
+ React is a peer dependency, and only the `/react` entry needs it. Install React 18.2 or 19 if you use
17
+ that entry.
18
+
19
+ ## Where credentials may live
20
+
21
+ An app key and app secret authorize an *application*, so they belong to server-side code. Anything
22
+ marked `NEXT_PUBLIC_*` is inlined into the browser bundle your players download, which publishes the
23
+ secret. In a browser, keep the secret on your own backend and send the player a short-lived token
24
+ instead.
25
+
26
+ ## Configure a client
27
+
28
+ `initializeSDK` sets the process-wide client that `fetchClient` proxies to:
29
+
30
+ ```ts
31
+ import { fetchClient, initializeSDK } from '@fzgame/sdk'
32
+
33
+ initializeSDK({
34
+ apiUrl: 'https://api.example.com',
35
+ appKey: process.env.FZ_APP_KEY,
36
+ appSecret: process.env.FZ_APP_SECRET,
37
+ })
38
+
39
+ const { data, error } = await fetchClient.GET('/sdk/app/current')
40
+ ```
41
+
42
+ `createSdkClient` returns an independent client, which is what to use when one process serves more
43
+ than one app:
44
+
45
+ ```ts
46
+ import { createSdkClient } from '@fzgame/sdk'
47
+
48
+ const tenantA = createSdkClient({ apiUrl: 'https://api.example.com', appKey: 'a', appSecret: 'sa' })
49
+ const tenantB = createSdkClient({ apiUrl: 'https://api.example.com', appKey: 'b', appSecret: 'sb' })
50
+ ```
51
+
52
+ From React:
53
+
54
+ ```tsx
55
+ import { SDKInitializer } from '@fzgame/sdk/react'
56
+
57
+ export function Providers() {
58
+ return <SDKInitializer apiUrl="https://api.example.com" appKey="..." appSecret="..." />
59
+ }
60
+ ```
61
+
62
+ The effect depends on each value, so a rotated secret or a tenant switch reconfigures the client
63
+ instead of being dropped after the first render.
64
+
65
+ ### `InitOptions`
66
+
67
+ | Option | Type | Meaning |
68
+ | --- | --- | --- |
69
+ | `apiUrl` | `string` | Base URL of the API. Required; a trailing slash is trimmed. |
70
+ | `appKey` | `string` | Value of the `x-app-key` header: the app's `id`, which is what the guard looks the app up by. |
71
+ | `appSecret` | `string` | Value of the `x-app-secret` header: one of the app's credential `clientSecret` values. |
72
+ | `timeoutMs` | `number` | Abort a request after this many milliseconds. `0` or unset means no deadline. |
73
+ | `fetch` | `typeof fetch` | Override the fetch implementation, for a Node polyfill or a test. |
74
+
75
+ Credentials travel in headers, which the server's OpenAPI document does not declare, so attach them
76
+ with `initializeSDK` or `createSdkClient` rather than by hand. The exported header names are
77
+ `APP_KEY_HEADER` (`x-app-key`) and `APP_SECRET_HEADER` (`x-app-secret`) if you need to set them
78
+ yourself.
79
+
80
+ A timeout is combined with any `AbortSignal` you pass to a request, so your own cancellation keeps
81
+ working.
82
+
83
+ ## Integrate a game into your app
84
+
85
+ The path from a configured client to a resumed play session. This code runs on your server; the
86
+ browser receives a play URL and never sees the app secret.
87
+
88
+ | Step | Call | Returns |
89
+ | --- | --- | --- |
90
+ | List the games published in the app | `GET /sdk/game-instances` | instances, each with its `game` |
91
+ | Identify or create the player | `POST /sdk/login` | `{ player, playerAccessToken }` |
92
+ | Read the player's live session, for a continue screen | `GET /sdk/sessions/active` | `{ session }`, the newest live session or `null` |
93
+ | Build the play URL | `resolveGameURL(...)` | `{resolvedUrl}?apiUrl=…&gameInstanceId=…&playerAccessToken=…` |
94
+ | Game connects, play starts or resumes | `POST /games/runtime/sessions` | the session, used by the kit |
95
+ | Game saves progress | `PUT /games/runtime/sessions/{token}/snapshot` | the session |
96
+
97
+ ### 1. List the games
98
+
99
+ ```ts
100
+ const { data, error } = await fetchClient.GET('/sdk/game-instances', {
101
+ params: { query: { page: 1, limit: 20 } },
102
+ })
103
+ const instances = data?.data ?? []
104
+ const instance = instances.find((i) => i.game?.slug === 'flabby-bird')
105
+ ```
106
+
107
+ The response is paginated: `data.data` holds the instances and `data.pageInfo` holds `page`,
108
+ `total`, `hasNextPage`, and related counts. Only instances in `PUBLISHED` status for the client's app
109
+ come back. Each item carries `id`, `resolvedUrl`, `overrides`, `status`, and a `game` relation.
110
+ `game.configSchema` is JSON; read `viewMode` out of it (`'landscape'` or `'portrait'`) to tell the two
111
+ apart.
112
+
113
+ ### 2. Identify the player
114
+
115
+ ```ts
116
+ const { data: login } = await fetchClient.POST('/sdk/login', {
117
+ body: { identity: 'user-42', displayName: 'Ada' },
118
+ })
119
+ const player = login?.player
120
+ const playerAccessToken = login?.playerAccessToken
121
+ ```
122
+
123
+ `identity` is your own user id. `AppPlayer` is unique per app on that value, so sending it on every
124
+ call returns the same player. `playerAccessToken` is a signed JWT for that player, bound to your app
125
+ and expiring with the play-session window; pass it to the game, never the app secret.
126
+
127
+ ### 3. Build the play URL
128
+
129
+ ```ts
130
+ const url = resolveGameURL(instance, login.playerAccessToken)
131
+ ```
132
+
133
+ `resolveGameURL` starts from `instance.resolvedUrl` and appends `apiUrl` (the one passed to
134
+ `initializeSDK`), `gameInstanceId`, `playerAccessToken`, and the instance `overrides` as `gameSetting`.
135
+ `apiUrl` must be reachable from the browser, because the frame calls the public instance and runtime
136
+ endpoints itself. A `createSdkClient` caller, or one that needs a different frame origin, passes it in
137
+ the optional third argument: `resolveGameURL(instance, token, { apiUrl, config })`, where `config`
138
+ replaces the instance overrides. The kit reads the token when the game calls `createKit()`; nothing
139
+ has to be sent into the frame first.
140
+
141
+ The token is a bearer credential: anyone holding it can play as that player, so keep it out of logs
142
+ and analytics. A host that holds a `playSessionToken` already can also post the kit's `session`
143
+ message after the frame loads (see the kit README).
144
+
145
+ ### 4. Landscape games
146
+
147
+ On a phone, present the frame rotated when `viewMode` is `'landscape'`. The API reports the value
148
+ only; rotation belongs to the host. The portal's `GameViewport` rotates the iframe with a CSS
149
+ transform on coarse-pointer devices, and the SDK plays no part in it.
150
+
151
+ ### 5. Play and resume
152
+
153
+ The game calls `POST /games/runtime/sessions` with `{ gameInstanceId, playerAccessToken }`. The
154
+ server verifies the JWT with the same key as `BETTER_AUTH_SECRET`, resolves the player it names, and
155
+ creates a session or returns the player's newest `CREATED`, `READY`, `PLAYING`, or `PAUSED` session
156
+ for the instance, so a returning player lands back in the same round with `kit.snapshot.state`
157
+ restored from the session's snapshot. The host does not create the session and does not look it up.
158
+
159
+ A host that shows its own "continue" or progress screen reads the same session first:
160
+
161
+ ```ts
162
+ const { data } = await fetchClient.GET('/sdk/sessions/active', {
163
+ params: { query: { playerId: player.id, gameInstanceId: instance.id } },
164
+ })
165
+ // data.session is the newest live session, or null
166
+ ```
167
+
168
+ `playerId` is the `AppPlayer.id` (the row id) and `playerAccessToken` is the JWT login returned. Pass
169
+ `gameInstanceId` to scope the answer to one build. A live session carries `token`, `status`,
170
+ `metadata.snapshot`, `score`, and `result`, so a host can show "your last round" before the frame
171
+ loads. The numbers and state the game reports stay authoritative: the host reads, the game writes.
172
+
173
+ A session ends on `finish` or `abort`, and a terminal session is not resumed. Closing the frame
174
+ leaves it resumable. Sessions expire after 24 hours, and `POST /sdk/players/{id}/logout` expires
175
+ every live session of a player.
176
+
177
+ ### What this package does not cover
178
+
179
+ - The frame side of the flow. `postMessage`, the snapshot, and the lifecycle events belong to
180
+ `@fzgame/kit`; this package calls the HTTP API.
181
+ - Landscape presentation. The API reports `viewMode` inside `game.configSchema`; rotating the frame
182
+ is the host's job, and the portal's `GameViewport` is the reference.
183
+ - `POST /sdk/sessions` creates a session from the host when you need the token up front. The session
184
+ events are `/sdk/sessions/{token}/start|pause|resume|score|finish|abort`.
185
+
186
+ ## Errors
187
+
188
+ | Error | Thrown when |
189
+ | --- | --- |
190
+ | `SdkConfigError` | `apiUrl` is missing or blank. |
191
+ | `SdkNotInitializedError` | A call on `fetchClient` happens before `initializeSDK` or `<SDKInitializer>` ran. |
192
+
193
+ Both are exported so a caller can branch on them. `fetchClient` throws rather than sending a request
194
+ to a placeholder origin, so a mis-ordered import fails where it happens.
195
+
196
+ Failed requests do not throw: `openapi-fetch` returns `{ data, error }`, and `error` carries the
197
+ status and the parsed body.
198
+
199
+ ## Types
200
+
201
+ `@fzgame/sdk` re-exports the generated types, so you can name a response shape without importing the
202
+ document separately:
203
+
204
+ ```ts
205
+ import { resolveGameURL, type GameInstance } from '@fzgame/sdk'
206
+ import type { components, paths } from '@fzgame/sdk'
207
+
208
+ type App = components['schemas']['App']
209
+ type GameInstanceId = paths['/sdk/game-instances/{id}']['get']['parameters']['path']['id']
210
+
211
+ // GameInstance is components['schemas']['AppGameInstance']: an item from GET /sdk/game-instances.
212
+ const playUrl = resolveGameURL(gameInstance, playerAccessToken)
213
+ ```
214
+
215
+ `src/types/schema.d.ts` is generated from the server document that the workspace build writes to
216
+ `apps/server/public/openapi-sdk.json`. Regenerate it after a server API change:
217
+
218
+ ```bash
219
+ pnpm --filter @fzgame/sdk compile
220
+ ```
221
+
222
+ That path is inside this monorepo, so the script runs for contributors here. Consumers of the
223
+ published package get the types that were generated at release time.
224
+
225
+ A route without a declared success response types its body as empty. When you add a controller
226
+ response, annotate it with `@ApiResponse` so the regenerated types carry it.
227
+
228
+ ## Entry points
229
+
230
+ | Specifier | Contents |
231
+ | --- | --- |
232
+ | `@fzgame/sdk` | Client, errors, header names, generated types. No React. |
233
+ | `@fzgame/sdk/react` | `SDKInitializer`. Requires React 18.2 or 19. |
234
+ | `@fzgame/sdk/package.json` | The manifest, for tooling that reads it. |
235
+
236
+ Both entries ship ESM and CommonJS, each with matching type declarations.
237
+
238
+ ## Development
239
+
240
+ ```bash
241
+ pnpm --filter @fzgame/sdk check-types
242
+ pnpm --filter @fzgame/sdk test:run
243
+ pnpm --filter @fzgame/sdk build
244
+ pnpm --filter @fzgame/sdk smoke # packs the tarball and consumes it from CJS and ESM
245
+ ```
246
+
247
+ ## License
248
+
249
+ ISC