@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 +15 -0
- package/README.md +248 -2
- package/dist/index.d.mts +1381 -0
- package/dist/index.d.ts +1381 -0
- package/dist/index.js +144 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +100 -0
- package/dist/index.mjs.map +1 -0
- package/dist/react.d.mts +11 -0
- package/dist/react.d.ts +11 -0
- package/dist/react.js +117 -0
- package/dist/react.js.map +1 -0
- package/dist/react.mjs +83 -0
- package/dist/react.mjs.map +1 -0
- package/package.json +91 -4
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
|
-
#
|
|
1
|
+
# @fzgame/sdk
|
|
2
2
|
|
|
3
|
-
|
|
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
|