@fzgame/sdk 0.1.0 → 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/README.md CHANGED
@@ -80,6 +80,109 @@ yourself.
80
80
  A timeout is combined with any `AbortSignal` you pass to a request, so your own cancellation keeps
81
81
  working.
82
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
+
83
186
  ## Errors
84
187
 
85
188
  | Error | Thrown when |
@@ -99,10 +202,14 @@ status and the parsed body.
99
202
  document separately:
100
203
 
101
204
  ```ts
205
+ import { resolveGameURL, type GameInstance } from '@fzgame/sdk'
102
206
  import type { components, paths } from '@fzgame/sdk'
103
207
 
104
208
  type App = components['schemas']['App']
105
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)
106
213
  ```
107
214
 
108
215
  `src/types/schema.d.ts` is generated from the server document that the workspace build writes to
@@ -115,6 +222,9 @@ pnpm --filter @fzgame/sdk compile
115
222
  That path is inside this monorepo, so the script runs for contributors here. Consumers of the
116
223
  published package get the types that were generated at release time.
117
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
+
118
228
  ## Entry points
119
229
 
120
230
  | Specifier | Contents |