@pixelume/sdk 0.3.0 → 0.4.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 +43 -1
- package/README.md +73 -19
- package/dist/contracts/index.d.ts +42 -2
- package/dist/contracts/index.js +107 -8
- package/dist/contracts/index.js.map +1 -1
- package/dist/core/PixelumeClient.d.ts +12 -2
- package/dist/core/PixelumeClient.js +16 -2
- package/dist/core/PixelumeClient.js.map +1 -1
- package/dist/core/config.d.ts +2 -1
- package/dist/core/config.js +8 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/errors.d.ts +44 -2
- package/dist/core/errors.js +30 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/types.d.ts +12 -0
- package/dist/core/types.js.map +1 -1
- package/dist/http/HttpClient.js +1 -0
- package/dist/http/HttpClient.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/auth/AuthModule.d.ts +28 -3
- package/dist/modules/auth/AuthModule.js +82 -40
- package/dist/modules/auth/AuthModule.js.map +1 -1
- package/dist/modules/auth/SessionManager.d.ts +91 -0
- package/dist/modules/auth/SessionManager.js +475 -0
- package/dist/modules/auth/SessionManager.js.map +1 -0
- package/dist/modules/saves/SavesModule.d.ts +100 -0
- package/dist/modules/saves/SavesModule.js +238 -0
- package/dist/modules/saves/SavesModule.js.map +1 -0
- package/dist/ui/widget.js +61 -4
- package/dist/ui/widget.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,47 @@ conter mudanças incompatíveis, sempre listadas em **Changed/Removed**.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-10-14
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Renovação automática de sessão com os refresh tokens rotativos da Pixelume API: renovação proativa
|
|
15
|
+
(`expires_in − 60 s`, com jitter), verificação ao retomar a aba (`visibilitychange`, `pageshow`,
|
|
16
|
+
`online`), single-flight por instância, uma renovação + um único retry após `401`, retry único após
|
|
17
|
+
falha de rede/5xx, backoff para `429`/offline sem logout e encerramento explícito em `invalid_grant`
|
|
18
|
+
(`session_expired`) ou `invalid_client` (`game_unavailable`). Tokens permanecem só em memória.
|
|
19
|
+
- `pixelume.auth.session`, `auth.subscribeSession()` e `auth.refresh()` para interfaces próprias
|
|
20
|
+
(escopos, expiração, renovação em andamento e falha transitória, sem expor tokens).
|
|
21
|
+
- `pixelume.saves` (Cloud Saves): `list()`, `load()`, `save()` e `delete()` sobre
|
|
22
|
+
`/v1/games/:gameId/saves`, com validação local de slot/versão/revisão/JSON, `PixelumeSaveConflictError`
|
|
23
|
+
(`409`, com `current` e `expectedRevision`), exclusão idempotente (`'deleted' | 'not_found'`) e
|
|
24
|
+
atividade observável (`saves.activity`, `saves.subscribe()`) sem dados do save.
|
|
25
|
+
- Opção `scope` (`'profile' | 'profile saves'`) no cliente e em `auth.login()`; a restauração após
|
|
26
|
+
reload reutiliza o escopo concedido.
|
|
27
|
+
- `PixelumeClient.destroy()` para liberar timers e listeners.
|
|
28
|
+
- `PixelumeHttpError.details` com os detalhes não sensíveis do envelope de erro da API
|
|
29
|
+
(`oauthError`, `quota`, metadados de conflito).
|
|
30
|
+
- Widget: aviso de conexão durante falhas transitórias de renovação, mensagem para jogo indisponível e
|
|
31
|
+
status da nuvem somente após `saves.save()`/`saves.delete()` explícitos.
|
|
32
|
+
- Exemplo Vite com status da sessão, painel de Cloud Saves e modo `?headless`.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- `auth.logout()` revoga o refresh token (`token_type_hint=refresh_token`, com `client_id`), encerrando
|
|
37
|
+
toda a autorização do jogo; sem refresh token, revoga o access token como antes.
|
|
38
|
+
- `auth.getPlayer()` renova a sessão em vez de declará-la expirada quando o access token vence ou a API
|
|
39
|
+
responde `401`; quando a sessão realmente termina, o estado `anonymous/expired` agora traz
|
|
40
|
+
`PixelumeAuthError` explícito em `error` (antes `null`).
|
|
41
|
+
- O parser do token aceita `scope` `profile saves` e os campos opcionais `refresh_token` e
|
|
42
|
+
`refresh_token_expires_in`.
|
|
43
|
+
- Uniões públicas ampliadas (aditivo, mas afeta `switch` exaustivo): `PixelumeAuthErrorReason` ganhou
|
|
44
|
+
`not_authenticated`, `session_expired`, `game_unavailable` e `renewal_rejected`;
|
|
45
|
+
`PixelumeApiErrorCode` ganhou os códigos de Cloud Saves e `OAUTH_UNSUPPORTED_GRANT_TYPE`.
|
|
46
|
+
- A interface `PixelumeAuthApi` ganhou `session`, `subscribeSession()` e `refresh()`; implementações
|
|
47
|
+
próprias dessa interface (mocks) precisam acrescentá-los.
|
|
48
|
+
- O marcador não sensível em `sessionStorage` passou a guardar o escopo concedido (`profile` ou
|
|
49
|
+
`profile saves`); o valor antigo `1` continua aceito.
|
|
50
|
+
|
|
10
51
|
## [0.3.0] - 2026-10-13
|
|
11
52
|
|
|
12
53
|
### Added
|
|
@@ -65,7 +106,8 @@ conter mudanças incompatíveis, sempre listadas em **Changed/Removed**.
|
|
|
65
106
|
`invalid_response`) e `isPixelumeError()`.
|
|
66
107
|
- Build ESM + `.d.ts`, CI e workflow de publicação via npm Trusted Publishing.
|
|
67
108
|
|
|
68
|
-
[Unreleased]: https://github.com/ericksonnascimento/pixelume-sdk/compare/v0.
|
|
109
|
+
[Unreleased]: https://github.com/ericksonnascimento/pixelume-sdk/compare/v0.4.0...HEAD
|
|
110
|
+
[0.4.0]: https://github.com/ericksonnascimento/pixelume-sdk/compare/v0.3.0...v0.4.0
|
|
69
111
|
[0.3.0]: https://github.com/ericksonnascimento/pixelume-sdk/compare/v0.2.0...v0.3.0
|
|
70
112
|
[0.2.0]: https://github.com/ericksonnascimento/pixelume-sdk/compare/v0.1.1...v0.2.0
|
|
71
113
|
[0.1.1]: https://github.com/ericksonnascimento/pixelume-sdk/compare/v0.1.0...v0.1.1
|
package/README.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# @pixelume/sdk
|
|
2
2
|
|
|
3
|
-
SDK TypeScript para jogos web da Pixelume. O core headless continua sendo o padrão
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
`
|
|
3
|
+
SDK TypeScript para jogos web da Pixelume. O core headless continua sendo o padrão e há um widget
|
|
4
|
+
oficial opt-in em `@pixelume/sdk/ui`. Os dois modos usam a mesma autenticação centralizada com OAuth 2.0
|
|
5
|
+
Authorization Code + PKCE S256. A versão 0.4 adiciona **renovação automática de sessão** (refresh tokens
|
|
6
|
+
rotativos, só em memória) e **Cloud Saves** (`pixelume.saves`). Não há dependências de runtime, senha
|
|
7
|
+
no jogo nem `client_secret`.
|
|
7
8
|
|
|
8
9
|
## Instalação
|
|
9
10
|
|
|
@@ -19,12 +20,14 @@ CommonJS, use `await import('@pixelume/sdk')`.
|
|
|
19
20
|
Antes de integrar um jogo, registre na Pixelume API:
|
|
20
21
|
|
|
21
22
|
- um `client_id` público, normalmente igual ao `gameId`;
|
|
22
|
-
-
|
|
23
|
-
- cada callback completo por igualdade exata de scheme, host, porta, path e query.
|
|
23
|
+
- os escopos: `profile` (obrigatório) e `saves` (somente se o jogo usar Cloud Saves);
|
|
24
|
+
- cada callback completo por igualdade exata de scheme, host, porta, path e query, e as origens do jogo.
|
|
24
25
|
|
|
25
26
|
Não são aceitos curingas, prefixos ou callbacks enviados dinamicamente. O cliente local já cadastrado é
|
|
26
27
|
`pixelume-local-dev`, exclusivamente com `http://localhost:4173/oauth/callback` e
|
|
27
|
-
`http://127.0.0.1:4173/oauth/callback`. Jogos
|
|
28
|
+
`http://127.0.0.1:4173/oauth/callback`, apenas com `profile`. Jogos são cadastrados pelo Game Registry
|
|
29
|
+
da API (`npm run admin:games` no repositório `pixelume-api`; veja `docs/GAME_REGISTRY.md` de lá). Em
|
|
30
|
+
14/10/2026 nenhum cliente público tinha `saves` habilitado em produção.
|
|
28
31
|
|
|
29
32
|
## Uso headless
|
|
30
33
|
|
|
@@ -54,17 +57,52 @@ loginButton.addEventListener('click', () => {
|
|
|
54
57
|
|
|
55
58
|
logoutButton.addEventListener('click', async () => {
|
|
56
59
|
try {
|
|
57
|
-
await pixelume.auth.logout(); // revoga somente
|
|
60
|
+
await pixelume.auth.logout(); // revoga somente a autorização deste jogo
|
|
58
61
|
} catch (error) {
|
|
59
62
|
if (!isPixelumeError(error)) throw error;
|
|
60
63
|
showRecoverableError(error.kind);
|
|
61
64
|
}
|
|
62
65
|
});
|
|
63
66
|
|
|
64
|
-
// Ao desmontar a tela:
|
|
67
|
+
// Ao desmontar a tela/jogo:
|
|
65
68
|
unsubscribe();
|
|
69
|
+
pixelume.destroy(); // cancela timers de renovação e listeners (não revoga)
|
|
66
70
|
```
|
|
67
71
|
|
|
72
|
+
### Sessões longas
|
|
73
|
+
|
|
74
|
+
Nada a fazer: o SDK renova o access token (10 min) antes de expirar, ao retomar a aba e após um `401`,
|
|
75
|
+
com uma única renovação por vez e um único retry da requisição. Falhas de rede mantêm o jogador
|
|
76
|
+
conectado (`pixelume.auth.session.error`); sessão revogada/expirada vira `anonymous/expired` com erro
|
|
77
|
+
explícito. Detalhes: [renovação de sessão](docs/SESSION_RENEWAL.md).
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
pixelume.auth.subscribeSession((session) => {
|
|
81
|
+
connectionBanner.hidden = session.error === null; // recuperável, sem logout
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Cloud Saves
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
const pixelume = new Pixelume({ gameId: 'rail-puzzle', scope: 'profile saves' });
|
|
89
|
+
await pixelume.initialize();
|
|
90
|
+
|
|
91
|
+
const current = await pixelume.saves.load<{ level: number }>('main'); // ou null
|
|
92
|
+
await pixelume.saves.save('main', {
|
|
93
|
+
schemaVersion: 1,
|
|
94
|
+
expectedRevision: current?.revision ?? 0, // 0 cria; n atualiza só a revisão n
|
|
95
|
+
data: { level: 12 },
|
|
96
|
+
});
|
|
97
|
+
const { saves, limits, usage } = await pixelume.saves.list();
|
|
98
|
+
await pixelume.saves.delete('old-slot', { expectedRevision: 3 }); // 'deleted' | 'not_found'
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Conflitos (`409`) rejeitam com `PixelumeSaveConflictError` (revisão remota em `error.current`); o SDK
|
|
102
|
+
nunca sobrescreve, mescla ou reenvia sozinho, e não há fila offline. Saves são dados pessoais não
|
|
103
|
+
verificados: não servem como prova de conquista, ranking ou prêmio. Guia completo:
|
|
104
|
+
[Cloud Saves](docs/CLOUD_SAVES.md).
|
|
105
|
+
|
|
68
106
|
Importar ou instanciar o core não cria elementos, não injeta estilos e não ativa UI. O health check
|
|
69
107
|
também é totalmente independente: chamar `/health` nunca monta o widget.
|
|
70
108
|
|
|
@@ -98,7 +136,9 @@ widget.destroy();
|
|
|
98
136
|
O painel usa Shadow DOM aberto, não aplica CSS global e chama somente `pixelume.auth.login()` e
|
|
99
137
|
`pixelume.auth.logout()`. Entrar sempre redireciona ao portal Pixelume Auth; o widget não exibe campos de
|
|
100
138
|
e-mail ou senha. Para overlays próprios ou canvas fullscreen, passe `container`, ajuste `position`,
|
|
101
|
-
`offset` e `zIndex`. Veja o [guia completo do widget](docs/phase-03-widget.md).
|
|
139
|
+
`offset` e `zIndex`. Veja o [guia completo do widget](docs/phase-03-widget.md). O widget também mostra
|
|
140
|
+
avisos de conexão durante a renovação e, apenas depois de `saves.save()`/`saves.delete()` explícitos, o
|
|
141
|
+
status da nuvem.
|
|
102
142
|
|
|
103
143
|
`pixelume.auth.state.status` é `uninitialized`, `redirecting`, `exchanging`, `anonymous` ou
|
|
104
144
|
`authenticated`. O estado anônimo informa `reason` (`visitor`, `cancelled`, `expired`, `logged_out` ou
|
|
@@ -107,14 +147,17 @@ e-mail ou senha. Para overlays próprios ou canvas fullscreen, passe `container`
|
|
|
107
147
|
### Visitante, recarga e expiração
|
|
108
148
|
|
|
109
149
|
- Sem login ou callback, `initialize()` termina como visitante e não faz requisição.
|
|
110
|
-
- O access token opaco dura 10 minutos e
|
|
150
|
+
- O access token opaco dura 10 minutos e é renovado automaticamente pelo refresh token rotativo; os dois
|
|
151
|
+
ficam **somente em memória**.
|
|
111
152
|
- Após autenticar, o SDK guarda em `sessionStorage` apenas o marcador não secreto de que aquela aba
|
|
112
|
-
tinha uma sessão. Em uma recarga, inicia uma nova autorização
|
|
113
|
-
continuar válida, o portal retorna sem pedir senha.
|
|
153
|
+
tinha uma sessão (e o escopo concedido). Em uma recarga, inicia uma nova autorização com o mesmo
|
|
154
|
+
escopo; enquanto a sessão central da API continuar válida, o portal retorna sem pedir senha.
|
|
155
|
+
`restoreSession: false` desliga esse comportamento.
|
|
114
156
|
- PKCE `state`/`code_verifier` ficam na sessão por no máximo 10 minutos e são removidos antes da troca.
|
|
115
157
|
- Código expirado/repetido, `state` divergente, callback adulterado e 401 deixam o jogo utilizável como
|
|
116
158
|
visitante. O jogo pode oferecer o login novamente.
|
|
117
|
-
- `logout()` limpa o estado local imediatamente
|
|
159
|
+
- `logout()` limpa o estado local imediatamente, cancela a renovação e revoga o refresh token em
|
|
160
|
+
`/v1/oauth/revoke` (o que encerra todos os tokens daquela autorização). A sessão central do portal
|
|
118
161
|
continua existente por desenho; entrar novamente pode ser imediato.
|
|
119
162
|
|
|
120
163
|
## Configuração
|
|
@@ -126,6 +169,7 @@ e-mail ou senha. Para overlays próprios ou canvas fullscreen, passe `container`
|
|
|
126
169
|
| `gameId` | — | slug público do jogo; também é o `clientId` padrão |
|
|
127
170
|
| `clientId` | `gameId` | identificador OAuth público cadastrado; nunca é credencial |
|
|
128
171
|
| `redirectUri` | `<origin>/oauth/callback` no browser | deve coincidir exatamente com o cadastro e não pode conter fragmento nem parâmetros OAuth |
|
|
172
|
+
| `scope` | `profile` | `profile saves` para Cloud Saves (o cadastro do jogo precisa permitir `saves`) |
|
|
129
173
|
| `restoreSession` | `true` | reautoriza após reload da mesma aba sem persistir token |
|
|
130
174
|
| `timeoutMs` | `10000` | cobre resposta e leitura do corpo |
|
|
131
175
|
| `credentials` | `omit` | política dos módulos genéricos; Auth força `omit` para token/userinfo/revoke |
|
|
@@ -150,16 +194,20 @@ uma ferramenta de teste, não UI oficial.
|
|
|
150
194
|
|
|
151
195
|
## Segurança e confiança
|
|
152
196
|
|
|
153
|
-
- O fluxo não é OIDC: não há ID Token, discovery
|
|
197
|
+
- O fluxo não é OIDC: não há ID Token, discovery nem `client_secret`. O refresh token é rotativo, de uso
|
|
198
|
+
único, ligado ao jogo e à sessão central, e fica só em memória.
|
|
154
199
|
- A sessão central é cookie `HttpOnly`, `Secure`, `SameSite=Lax` e host-only da API. O jogo nunca a lê.
|
|
155
200
|
- Requests OAuth do jogo usam Bearer curto e `credentials: 'omit'`; `credentials: 'include'` pertence
|
|
156
201
|
apenas ao portal ao consultar/concluir a sessão central.
|
|
157
202
|
- `crypto.getRandomValues` e Web Crypto SHA-256 geram `state`, verifier e challenge; não há
|
|
158
203
|
`Math.random`.
|
|
159
|
-
- Access token, código e verifier não são registrados. Tokens não vão para `localStorage`,
|
|
204
|
+
- Access/refresh token, código e verifier não são registrados. Tokens não vão para `localStorage`,
|
|
205
|
+
`sessionStorage`, cookies, URL, estado observável ou mensagens de erro.
|
|
160
206
|
- CORS/Origin não são considerados autenticação. PKCE, URI exata, state e vínculos persistidos pela API
|
|
161
207
|
são os controles de segurança.
|
|
162
208
|
- XSS ainda pode ler um token que esteja em memória; jogos devem manter CSP e dependências sob controle.
|
|
209
|
+
- Cloud Saves sempre passam pela autorização do backend (token do próprio jogo + escopo `saves`); o SDK
|
|
210
|
+
nunca envia `playerId`, e a UI nunca prova legitimidade de progresso.
|
|
163
211
|
|
|
164
212
|
## Erros
|
|
165
213
|
|
|
@@ -168,8 +216,11 @@ Todo erro lançado diretamente pelo SDK é `PixelumeError`. Os `kind` estáveis
|
|
|
168
216
|
o modo visitante; consulte `pixelume.auth.state.error`. Métodos chamados explicitamente (`login`,
|
|
169
217
|
`getPlayer`, `logout`) rejeitam normalmente.
|
|
170
218
|
|
|
171
|
-
O
|
|
172
|
-
|
|
219
|
+
O único retry automático é o da renovação de sessão: após `401` em uma rota autenticada, o SDK renova uma
|
|
220
|
+
vez e repete aquela requisição uma vez. Nada mais é repetido. Para 400/403/409/413/429/5xx, use
|
|
221
|
+
`PixelumeHttpError.status`, `code`, `details` e `requestId`; conflitos de save são
|
|
222
|
+
`PixelumeSaveConflictError`; sessões encerradas são `PixelumeAuthError` (`session_expired`,
|
|
223
|
+
`game_unavailable`, `renewal_rejected`, `not_authenticated`). Para cancelamento, use `AbortSignal`.
|
|
173
224
|
|
|
174
225
|
## Health check
|
|
175
226
|
|
|
@@ -184,6 +235,7 @@ local, mas não diretamente de outro domínio no browser.
|
|
|
184
235
|
|
|
185
236
|
| SDK | Pixelume API | Pixelume Auth | Recursos |
|
|
186
237
|
| ------- | ---------------------------------------------------- | ------------------------------ | ----------------------------------------------- |
|
|
238
|
+
| `0.4.x` | OpenAPI `0.6.0`, release `10752b8` ou compatível | Fase 2 `3a1ded4` ou compatível | Renovação de sessão, Cloud Saves, widget, SSO |
|
|
187
239
|
| `0.3.x` | OpenAPI `0.4.0`, release SSO `3846479` ou compatível | Fase 2 `3a1ded4` ou compatível | Widget opt-in, SSO PKCE, perfil, logout, health |
|
|
188
240
|
| `0.2.x` | OpenAPI `0.4.0`, release SSO `3846479` ou compatível | Fase 2 `3a1ded4` ou compatível | SSO PKCE, perfil, logout, health |
|
|
189
241
|
| `0.1.x` | API anterior/atual para `/health` | não consumido | fundação e health apenas |
|
|
@@ -201,7 +253,9 @@ npm run verify
|
|
|
201
253
|
npm run example:smoke # leitura opcional da API real
|
|
202
254
|
```
|
|
203
255
|
|
|
204
|
-
Mais detalhes: [
|
|
256
|
+
Mais detalhes: [renovação de sessão](docs/SESSION_RENEWAL.md), [Cloud Saves](docs/CLOUD_SAVES.md),
|
|
257
|
+
[auditoria da Fase 4](docs/PHASE_04_CONTRACT_AUDIT.md), [widget oficial](docs/phase-03-widget.md),
|
|
258
|
+
[integração SSO](docs/SSO_INTEGRATION.md),
|
|
205
259
|
[auditoria do contrato](docs/SSO_CONTRACT_AUDIT.md), [arquitetura](docs/architecture.md) e
|
|
206
260
|
[publicação](docs/publishing.md).
|
|
207
261
|
|
|
@@ -25,11 +25,17 @@ export interface PlayerEnvelopeWire {
|
|
|
25
25
|
player: PlayerWire;
|
|
26
26
|
}
|
|
27
27
|
export declare const parsePlayerEnvelope: BodyParser<PlayerEnvelopeWire>;
|
|
28
|
+
/** Scopes the API can grant. `profile` is always present; `saves` is opt-in per game. */
|
|
29
|
+
export type OAuthScopeWire = 'profile' | 'saves';
|
|
28
30
|
export interface OAuthTokenResponseWire {
|
|
29
31
|
access_token: string;
|
|
30
32
|
token_type: 'Bearer';
|
|
31
33
|
expires_in: number;
|
|
32
|
-
scope
|
|
34
|
+
/** Granted scopes, parsed from the space-separated `scope` field. */
|
|
35
|
+
scopes: readonly OAuthScopeWire[];
|
|
36
|
+
/** Rotating refresh token; absent for legacy codes or when the central session already ended. */
|
|
37
|
+
refresh_token?: string;
|
|
38
|
+
refresh_token_expires_in?: number;
|
|
33
39
|
}
|
|
34
40
|
export declare const parseOAuthTokenResponse: BodyParser<OAuthTokenResponseWire>;
|
|
35
41
|
export declare const parseEmptyResponse: BodyParser<void>;
|
|
@@ -40,13 +46,47 @@ export interface HealthResponseWire {
|
|
|
40
46
|
release?: string;
|
|
41
47
|
}
|
|
42
48
|
export declare const parseHealthResponse: BodyParser<HealthResponseWire>;
|
|
43
|
-
/** Error envelope returned by every API error: `{ error: { code, message, requestId } }`. */
|
|
49
|
+
/** Error envelope returned by every API error: `{ error: { code, message, requestId, details? } }`. */
|
|
44
50
|
export interface ErrorEnvelopeWire {
|
|
45
51
|
error: {
|
|
46
52
|
code: string;
|
|
47
53
|
message: string;
|
|
48
54
|
requestId: string;
|
|
55
|
+
details: Readonly<Record<string, unknown>> | null;
|
|
49
56
|
};
|
|
50
57
|
}
|
|
51
58
|
/** Returns the envelope when `body` matches it, `null` otherwise (e.g. an HTML 502 from a proxy). */
|
|
52
59
|
export declare function parseErrorEnvelope(body: unknown): ErrorEnvelopeWire | null;
|
|
60
|
+
export interface SaveMetadataWire {
|
|
61
|
+
gameId: string;
|
|
62
|
+
slotId: string;
|
|
63
|
+
schemaVersion: number;
|
|
64
|
+
revision: number;
|
|
65
|
+
sizeBytes: number;
|
|
66
|
+
createdAt: string;
|
|
67
|
+
updatedAt: string;
|
|
68
|
+
}
|
|
69
|
+
export interface CloudSaveWire extends SaveMetadataWire {
|
|
70
|
+
data: Record<string, unknown>;
|
|
71
|
+
}
|
|
72
|
+
export interface SaveListWire {
|
|
73
|
+
gameId: string;
|
|
74
|
+
saves: SaveMetadataWire[];
|
|
75
|
+
limits: {
|
|
76
|
+
maxSlotsPerGame: number;
|
|
77
|
+
maxDataBytes: number;
|
|
78
|
+
maxBytesPerPlayer: number;
|
|
79
|
+
};
|
|
80
|
+
usage: {
|
|
81
|
+
slots: number;
|
|
82
|
+
playerBytes: number;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Validates one `SaveMetadata` object; `null` when it does not match. */
|
|
86
|
+
export declare function parseSaveMetadata(value: unknown): SaveMetadataWire | null;
|
|
87
|
+
/** `PUT` → `{ save: SaveMetadata }`. */
|
|
88
|
+
export declare const parseSaveMetadataEnvelope: BodyParser<SaveMetadataWire>;
|
|
89
|
+
/** `GET /saves/:slotId` → `{ save: SaveMetadata & { data } }`. */
|
|
90
|
+
export declare const parseCloudSaveEnvelope: BodyParser<CloudSaveWire>;
|
|
91
|
+
/** `GET /saves` → `{ gameId, saves, limits, usage }`. */
|
|
92
|
+
export declare const parseSaveList: BodyParser<SaveListWire>;
|
package/dist/contracts/index.js
CHANGED
|
@@ -27,11 +27,12 @@ export const parsePlayerEnvelope = (body) => {
|
|
|
27
27
|
}
|
|
28
28
|
return ok({ player: { id, displayName, avatarUrl } });
|
|
29
29
|
};
|
|
30
|
+
const TOKEN_PATTERN = /^[A-Za-z0-9_-]{43,128}$/u;
|
|
31
|
+
const KNOWN_SCOPES = ['profile', 'saves'];
|
|
30
32
|
export const parseOAuthTokenResponse = (body) => {
|
|
31
33
|
if (!isRecord(body))
|
|
32
34
|
return fail('expected a JSON object');
|
|
33
|
-
if (typeof body.access_token !== 'string' ||
|
|
34
|
-
!/^[A-Za-z0-9_-]{43,128}$/u.test(body.access_token)) {
|
|
35
|
+
if (typeof body.access_token !== 'string' || !TOKEN_PATTERN.test(body.access_token)) {
|
|
35
36
|
return fail('expected "access_token" to be a base64url token');
|
|
36
37
|
}
|
|
37
38
|
if (body.token_type !== 'Bearer')
|
|
@@ -39,13 +40,34 @@ export const parseOAuthTokenResponse = (body) => {
|
|
|
39
40
|
if (!Number.isInteger(body.expires_in) || body.expires_in <= 0) {
|
|
40
41
|
return fail('expected "expires_in" to be a positive integer');
|
|
41
42
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const scopes = typeof body.scope === 'string' ? body.scope.split(' ') : [];
|
|
44
|
+
if (!scopes.includes('profile') ||
|
|
45
|
+
new Set(scopes).size !== scopes.length ||
|
|
46
|
+
!scopes.every((scope) => KNOWN_SCOPES.includes(scope))) {
|
|
47
|
+
return fail('expected "scope" to be "profile" or "profile saves"');
|
|
48
|
+
}
|
|
49
|
+
const result = {
|
|
45
50
|
access_token: body.access_token,
|
|
46
51
|
token_type: 'Bearer',
|
|
47
52
|
expires_in: body.expires_in,
|
|
48
|
-
|
|
53
|
+
scopes: scopes,
|
|
54
|
+
};
|
|
55
|
+
if (body.refresh_token === undefined)
|
|
56
|
+
return ok(result);
|
|
57
|
+
if (typeof body.refresh_token !== 'string' || !TOKEN_PATTERN.test(body.refresh_token)) {
|
|
58
|
+
return fail('expected "refresh_token" to be a base64url token when present');
|
|
59
|
+
}
|
|
60
|
+
const refreshExpiresIn = body.refresh_token_expires_in;
|
|
61
|
+
if (refreshExpiresIn !== undefined &&
|
|
62
|
+
(!Number.isInteger(refreshExpiresIn) || refreshExpiresIn < 0)) {
|
|
63
|
+
return fail('expected "refresh_token_expires_in" to be a non-negative integer when present');
|
|
64
|
+
}
|
|
65
|
+
return ok({
|
|
66
|
+
...result,
|
|
67
|
+
refresh_token: body.refresh_token,
|
|
68
|
+
...(refreshExpiresIn === undefined
|
|
69
|
+
? {}
|
|
70
|
+
: { refresh_token_expires_in: refreshExpiresIn }),
|
|
49
71
|
});
|
|
50
72
|
};
|
|
51
73
|
export const parseEmptyResponse = (body) => body === undefined ? ok(undefined) : fail('expected an empty response body');
|
|
@@ -69,10 +91,87 @@ export const parseHealthResponse = (body) => {
|
|
|
69
91
|
export function parseErrorEnvelope(body) {
|
|
70
92
|
if (!isRecord(body) || !isRecord(body.error))
|
|
71
93
|
return null;
|
|
72
|
-
const { code, message, requestId } = body.error;
|
|
94
|
+
const { code, message, requestId, details } = body.error;
|
|
73
95
|
if (typeof code !== 'string' || typeof message !== 'string' || typeof requestId !== 'string') {
|
|
74
96
|
return null;
|
|
75
97
|
}
|
|
76
|
-
return { error: { code, message, requestId } };
|
|
98
|
+
return { error: { code, message, requestId, details: isRecord(details) ? details : null } };
|
|
99
|
+
}
|
|
100
|
+
function isNonNegativeInteger(value) {
|
|
101
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
102
|
+
}
|
|
103
|
+
function isTimestamp(value) {
|
|
104
|
+
return typeof value === 'string' && !Number.isNaN(Date.parse(value));
|
|
105
|
+
}
|
|
106
|
+
/** Validates one `SaveMetadata` object; `null` when it does not match. */
|
|
107
|
+
export function parseSaveMetadata(value) {
|
|
108
|
+
if (!isRecord(value))
|
|
109
|
+
return null;
|
|
110
|
+
const { gameId, slotId, schemaVersion, revision, sizeBytes, createdAt, updatedAt } = value;
|
|
111
|
+
if (typeof gameId !== 'string' ||
|
|
112
|
+
typeof slotId !== 'string' ||
|
|
113
|
+
!isNonNegativeInteger(schemaVersion) ||
|
|
114
|
+
schemaVersion < 1 ||
|
|
115
|
+
!isNonNegativeInteger(revision) ||
|
|
116
|
+
revision < 1 ||
|
|
117
|
+
!isNonNegativeInteger(sizeBytes) ||
|
|
118
|
+
!isTimestamp(createdAt) ||
|
|
119
|
+
!isTimestamp(updatedAt)) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
return { gameId, slotId, schemaVersion, revision, sizeBytes, createdAt, updatedAt };
|
|
77
123
|
}
|
|
124
|
+
/** `PUT` → `{ save: SaveMetadata }`. */
|
|
125
|
+
export const parseSaveMetadataEnvelope = (body) => {
|
|
126
|
+
const save = isRecord(body) ? parseSaveMetadata(body.save) : null;
|
|
127
|
+
return save ? ok(save) : fail('expected a JSON object containing "save" metadata');
|
|
128
|
+
};
|
|
129
|
+
/** `GET /saves/:slotId` → `{ save: SaveMetadata & { data } }`. */
|
|
130
|
+
export const parseCloudSaveEnvelope = (body) => {
|
|
131
|
+
if (!isRecord(body) || !isRecord(body.save)) {
|
|
132
|
+
return fail('expected a JSON object containing "save"');
|
|
133
|
+
}
|
|
134
|
+
const metadata = parseSaveMetadata(body.save);
|
|
135
|
+
if (!metadata)
|
|
136
|
+
return fail('expected "save" to contain valid save metadata');
|
|
137
|
+
const data = body.save.data;
|
|
138
|
+
if (!isRecord(data))
|
|
139
|
+
return fail('expected "save.data" to be a JSON object');
|
|
140
|
+
return ok({ ...metadata, data });
|
|
141
|
+
};
|
|
142
|
+
/** `GET /saves` → `{ gameId, saves, limits, usage }`. */
|
|
143
|
+
export const parseSaveList = (body) => {
|
|
144
|
+
if (!isRecord(body) || typeof body.gameId !== 'string' || !Array.isArray(body.saves)) {
|
|
145
|
+
return fail('expected a JSON object containing "gameId" and "saves"');
|
|
146
|
+
}
|
|
147
|
+
const saves = [];
|
|
148
|
+
for (const entry of body.saves) {
|
|
149
|
+
const save = parseSaveMetadata(entry);
|
|
150
|
+
if (!save)
|
|
151
|
+
return fail('expected every entry of "saves" to be save metadata');
|
|
152
|
+
saves.push(save);
|
|
153
|
+
}
|
|
154
|
+
const { limits, usage } = body;
|
|
155
|
+
if (!isRecord(limits) ||
|
|
156
|
+
!isNonNegativeInteger(limits.maxSlotsPerGame) ||
|
|
157
|
+
!isNonNegativeInteger(limits.maxDataBytes) ||
|
|
158
|
+
!isNonNegativeInteger(limits.maxBytesPerPlayer)) {
|
|
159
|
+
return fail('expected "limits" to contain non-negative integers');
|
|
160
|
+
}
|
|
161
|
+
if (!isRecord(usage) ||
|
|
162
|
+
!isNonNegativeInteger(usage.slots) ||
|
|
163
|
+
!isNonNegativeInteger(usage.playerBytes)) {
|
|
164
|
+
return fail('expected "usage" to contain non-negative integers');
|
|
165
|
+
}
|
|
166
|
+
return ok({
|
|
167
|
+
gameId: body.gameId,
|
|
168
|
+
saves,
|
|
169
|
+
limits: {
|
|
170
|
+
maxSlotsPerGame: limits.maxSlotsPerGame,
|
|
171
|
+
maxDataBytes: limits.maxDataBytes,
|
|
172
|
+
maxBytesPerPlayer: limits.maxBytesPerPlayer,
|
|
173
|
+
},
|
|
174
|
+
usage: { slots: usage.slots, playerBytes: usage.playerBytes },
|
|
175
|
+
});
|
|
176
|
+
};
|
|
78
177
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/contracts/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Wire contracts of the Pixelume API used by the SDK, with hand-written runtime guards.\n *\n * Source of truth: `pixelume-api` (`docs/openapi.yaml` and `src/interfaces/http/app.ts`).\n * See `docs/api-contract-audit.md`. These types describe JSON on the wire and are internal;\n * public modules map them to SDK types so the API can evolve without breaking consumers.\n *\n * Validation is intentionally tolerant to *additive* changes (unknown fields are ignored,\n * unknown enum values are accepted as strings) and strict about the fields the SDK relies on.\n */\n\nexport type ParseResult<T> =\n { readonly ok: true; readonly value: T } | { readonly ok: false; readonly reason: string };\n\nexport type BodyParser<T> = (body: unknown) => ParseResult<T>;\n\nconst ok = <T>(value: T): ParseResult<T> => ({ ok: true, value });\nconst fail = (reason: string): ParseResult<never> => ({ ok: false, reason });\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nexport interface PlayerWire {\n id: string;\n displayName: string;\n avatarUrl: string | null;\n}\n\nexport interface PlayerEnvelopeWire {\n player: PlayerWire;\n}\n\nexport const parsePlayerEnvelope: BodyParser<PlayerEnvelopeWire> = (body) => {\n if (!isRecord(body) || !isRecord(body.player)) {\n return fail('expected a JSON object containing \"player\"');\n }\n const { id, displayName, avatarUrl } = body.player;\n if (typeof id !== 'string' || id.length === 0) return fail('expected \"player.id\" to be a string');\n if (typeof displayName !== 'string') return fail('expected \"player.displayName\" to be a string');\n if (avatarUrl !== null && typeof avatarUrl !== 'string') {\n return fail('expected \"player.avatarUrl\" to be a string or null');\n }\n return ok({ player: { id, displayName, avatarUrl } });\n};\n\nexport interface OAuthTokenResponseWire {\n access_token: string;\n token_type: 'Bearer';\n expires_in: number;\n scope: 'profile';\n}\n\nexport const parseOAuthTokenResponse: BodyParser<OAuthTokenResponseWire> = (body) => {\n if (!isRecord(body)) return fail('expected a JSON object');\n if (\n typeof body.access_token !== 'string' ||\n !/^[A-Za-z0-9_-]{43,128}$/u.test(body.access_token)\n ) {\n return fail('expected \"access_token\" to be a base64url token');\n }\n if (body.token_type !== 'Bearer') return fail('expected \"token_type\" to be \"Bearer\"');\n if (!Number.isInteger(body.expires_in) || (body.expires_in as number) <= 0) {\n return fail('expected \"expires_in\" to be a positive integer');\n }\n if (body.scope !== 'profile') return fail('expected \"scope\" to be \"profile\"');\n return ok({\n access_token: body.access_token,\n token_type: 'Bearer',\n expires_in: body.expires_in as number,\n scope: 'profile',\n });\n};\n\nexport const parseEmptyResponse: BodyParser<void> = (body) =>\n body === undefined ? ok(undefined) : fail('expected an empty response body');\n\n/** `GET /health` → 200. OpenAPI: `{ status: 'ok', registration: enum, release?: /^[a-f0-9]{7,40}$/ }`. */\nexport interface HealthResponseWire {\n status: 'ok';\n registration: string;\n release?: string;\n}\n\nexport const parseHealthResponse: BodyParser<HealthResponseWire> = (body) => {\n if (!isRecord(body)) return fail('expected a JSON object');\n if (body.status !== 'ok') return fail('expected \"status\" to be \"ok\"');\n if (typeof body.registration !== 'string') return fail('expected \"registration\" to be a string');\n if (body.release !== undefined && typeof body.release !== 'string') {\n return fail('expected \"release\" to be a string when present');\n }\n return ok({\n status: 'ok',\n registration: body.registration,\n ...(body.release === undefined ? {} : { release: body.release }),\n });\n};\n\n/** Error envelope returned by every API error: `{ error: { code, message, requestId } }`. */\nexport interface ErrorEnvelopeWire {\n error: { code: string; message: string; requestId: string };\n}\n\n/** Returns the envelope when `body` matches it, `null` otherwise (e.g. an HTML 502 from a proxy). */\nexport function parseErrorEnvelope(body: unknown): ErrorEnvelopeWire | null {\n if (!isRecord(body) || !isRecord(body.error)) return null;\n const { code, message, requestId } = body.error;\n if (typeof code !== 'string' || typeof message !== 'string' || typeof requestId !== 'string') {\n return null;\n }\n return { error: { code, message, requestId } };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/contracts/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Wire contracts of the Pixelume API used by the SDK, with hand-written runtime guards.\n *\n * Source of truth: `pixelume-api` (`docs/openapi.yaml` and `src/interfaces/http/app.ts`).\n * See `docs/api-contract-audit.md`. These types describe JSON on the wire and are internal;\n * public modules map them to SDK types so the API can evolve without breaking consumers.\n *\n * Validation is intentionally tolerant to *additive* changes (unknown fields are ignored,\n * unknown enum values are accepted as strings) and strict about the fields the SDK relies on.\n */\n\nexport type ParseResult<T> =\n { readonly ok: true; readonly value: T } | { readonly ok: false; readonly reason: string };\n\nexport type BodyParser<T> = (body: unknown) => ParseResult<T>;\n\nconst ok = <T>(value: T): ParseResult<T> => ({ ok: true, value });\nconst fail = (reason: string): ParseResult<never> => ({ ok: false, reason });\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nexport interface PlayerWire {\n id: string;\n displayName: string;\n avatarUrl: string | null;\n}\n\nexport interface PlayerEnvelopeWire {\n player: PlayerWire;\n}\n\nexport const parsePlayerEnvelope: BodyParser<PlayerEnvelopeWire> = (body) => {\n if (!isRecord(body) || !isRecord(body.player)) {\n return fail('expected a JSON object containing \"player\"');\n }\n const { id, displayName, avatarUrl } = body.player;\n if (typeof id !== 'string' || id.length === 0) return fail('expected \"player.id\" to be a string');\n if (typeof displayName !== 'string') return fail('expected \"player.displayName\" to be a string');\n if (avatarUrl !== null && typeof avatarUrl !== 'string') {\n return fail('expected \"player.avatarUrl\" to be a string or null');\n }\n return ok({ player: { id, displayName, avatarUrl } });\n};\n\n/** Scopes the API can grant. `profile` is always present; `saves` is opt-in per game. */\nexport type OAuthScopeWire = 'profile' | 'saves';\n\nexport interface OAuthTokenResponseWire {\n access_token: string;\n token_type: 'Bearer';\n expires_in: number;\n /** Granted scopes, parsed from the space-separated `scope` field. */\n scopes: readonly OAuthScopeWire[];\n /** Rotating refresh token; absent for legacy codes or when the central session already ended. */\n refresh_token?: string;\n refresh_token_expires_in?: number;\n}\n\nconst TOKEN_PATTERN = /^[A-Za-z0-9_-]{43,128}$/u;\nconst KNOWN_SCOPES: readonly string[] = ['profile', 'saves'];\n\nexport const parseOAuthTokenResponse: BodyParser<OAuthTokenResponseWire> = (body) => {\n if (!isRecord(body)) return fail('expected a JSON object');\n if (typeof body.access_token !== 'string' || !TOKEN_PATTERN.test(body.access_token)) {\n return fail('expected \"access_token\" to be a base64url token');\n }\n if (body.token_type !== 'Bearer') return fail('expected \"token_type\" to be \"Bearer\"');\n if (!Number.isInteger(body.expires_in) || (body.expires_in as number) <= 0) {\n return fail('expected \"expires_in\" to be a positive integer');\n }\n const scopes = typeof body.scope === 'string' ? body.scope.split(' ') : [];\n if (\n !scopes.includes('profile') ||\n new Set(scopes).size !== scopes.length ||\n !scopes.every((scope) => KNOWN_SCOPES.includes(scope))\n ) {\n return fail('expected \"scope\" to be \"profile\" or \"profile saves\"');\n }\n const result: OAuthTokenResponseWire = {\n access_token: body.access_token,\n token_type: 'Bearer',\n expires_in: body.expires_in as number,\n scopes: scopes as OAuthScopeWire[],\n };\n if (body.refresh_token === undefined) return ok(result);\n if (typeof body.refresh_token !== 'string' || !TOKEN_PATTERN.test(body.refresh_token)) {\n return fail('expected \"refresh_token\" to be a base64url token when present');\n }\n const refreshExpiresIn = body.refresh_token_expires_in;\n if (\n refreshExpiresIn !== undefined &&\n (!Number.isInteger(refreshExpiresIn) || (refreshExpiresIn as number) < 0)\n ) {\n return fail('expected \"refresh_token_expires_in\" to be a non-negative integer when present');\n }\n return ok({\n ...result,\n refresh_token: body.refresh_token,\n ...(refreshExpiresIn === undefined\n ? {}\n : { refresh_token_expires_in: refreshExpiresIn as number }),\n });\n};\n\nexport const parseEmptyResponse: BodyParser<void> = (body) =>\n body === undefined ? ok(undefined) : fail('expected an empty response body');\n\n/** `GET /health` → 200. OpenAPI: `{ status: 'ok', registration: enum, release?: /^[a-f0-9]{7,40}$/ }`. */\nexport interface HealthResponseWire {\n status: 'ok';\n registration: string;\n release?: string;\n}\n\nexport const parseHealthResponse: BodyParser<HealthResponseWire> = (body) => {\n if (!isRecord(body)) return fail('expected a JSON object');\n if (body.status !== 'ok') return fail('expected \"status\" to be \"ok\"');\n if (typeof body.registration !== 'string') return fail('expected \"registration\" to be a string');\n if (body.release !== undefined && typeof body.release !== 'string') {\n return fail('expected \"release\" to be a string when present');\n }\n return ok({\n status: 'ok',\n registration: body.registration,\n ...(body.release === undefined ? {} : { release: body.release }),\n });\n};\n\n/** Error envelope returned by every API error: `{ error: { code, message, requestId, details? } }`. */\nexport interface ErrorEnvelopeWire {\n error: {\n code: string;\n message: string;\n requestId: string;\n details: Readonly<Record<string, unknown>> | null;\n };\n}\n\n/** Returns the envelope when `body` matches it, `null` otherwise (e.g. an HTML 502 from a proxy). */\nexport function parseErrorEnvelope(body: unknown): ErrorEnvelopeWire | null {\n if (!isRecord(body) || !isRecord(body.error)) return null;\n const { code, message, requestId, details } = body.error;\n if (typeof code !== 'string' || typeof message !== 'string' || typeof requestId !== 'string') {\n return null;\n }\n return { error: { code, message, requestId, details: isRecord(details) ? details : null } };\n}\n\n// ---------------------------------------------------------------------------------------------\n// Cloud Saves (`/v1/games/:gameId/saves`). OpenAPI: SaveMetadata, CloudSave, SaveListResponse.\n// ---------------------------------------------------------------------------------------------\n\nexport interface SaveMetadataWire {\n gameId: string;\n slotId: string;\n schemaVersion: number;\n revision: number;\n sizeBytes: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface CloudSaveWire extends SaveMetadataWire {\n data: Record<string, unknown>;\n}\n\nexport interface SaveListWire {\n gameId: string;\n saves: SaveMetadataWire[];\n limits: { maxSlotsPerGame: number; maxDataBytes: number; maxBytesPerPlayer: number };\n usage: { slots: number; playerBytes: number };\n}\n\nfunction isNonNegativeInteger(value: unknown): value is number {\n return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;\n}\n\nfunction isTimestamp(value: unknown): value is string {\n return typeof value === 'string' && !Number.isNaN(Date.parse(value));\n}\n\n/** Validates one `SaveMetadata` object; `null` when it does not match. */\nexport function parseSaveMetadata(value: unknown): SaveMetadataWire | null {\n if (!isRecord(value)) return null;\n const { gameId, slotId, schemaVersion, revision, sizeBytes, createdAt, updatedAt } = value;\n if (\n typeof gameId !== 'string' ||\n typeof slotId !== 'string' ||\n !isNonNegativeInteger(schemaVersion) ||\n schemaVersion < 1 ||\n !isNonNegativeInteger(revision) ||\n revision < 1 ||\n !isNonNegativeInteger(sizeBytes) ||\n !isTimestamp(createdAt) ||\n !isTimestamp(updatedAt)\n ) {\n return null;\n }\n return { gameId, slotId, schemaVersion, revision, sizeBytes, createdAt, updatedAt };\n}\n\n/** `PUT` → `{ save: SaveMetadata }`. */\nexport const parseSaveMetadataEnvelope: BodyParser<SaveMetadataWire> = (body) => {\n const save = isRecord(body) ? parseSaveMetadata(body.save) : null;\n return save ? ok(save) : fail('expected a JSON object containing \"save\" metadata');\n};\n\n/** `GET /saves/:slotId` → `{ save: SaveMetadata & { data } }`. */\nexport const parseCloudSaveEnvelope: BodyParser<CloudSaveWire> = (body) => {\n if (!isRecord(body) || !isRecord(body.save)) {\n return fail('expected a JSON object containing \"save\"');\n }\n const metadata = parseSaveMetadata(body.save);\n if (!metadata) return fail('expected \"save\" to contain valid save metadata');\n const data = body.save.data;\n if (!isRecord(data)) return fail('expected \"save.data\" to be a JSON object');\n return ok({ ...metadata, data });\n};\n\n/** `GET /saves` → `{ gameId, saves, limits, usage }`. */\nexport const parseSaveList: BodyParser<SaveListWire> = (body) => {\n if (!isRecord(body) || typeof body.gameId !== 'string' || !Array.isArray(body.saves)) {\n return fail('expected a JSON object containing \"gameId\" and \"saves\"');\n }\n const saves: SaveMetadataWire[] = [];\n for (const entry of body.saves) {\n const save = parseSaveMetadata(entry);\n if (!save) return fail('expected every entry of \"saves\" to be save metadata');\n saves.push(save);\n }\n const { limits, usage } = body;\n if (\n !isRecord(limits) ||\n !isNonNegativeInteger(limits.maxSlotsPerGame) ||\n !isNonNegativeInteger(limits.maxDataBytes) ||\n !isNonNegativeInteger(limits.maxBytesPerPlayer)\n ) {\n return fail('expected \"limits\" to contain non-negative integers');\n }\n if (\n !isRecord(usage) ||\n !isNonNegativeInteger(usage.slots) ||\n !isNonNegativeInteger(usage.playerBytes)\n ) {\n return fail('expected \"usage\" to contain non-negative integers');\n }\n return ok({\n gameId: body.gameId,\n saves,\n limits: {\n maxSlotsPerGame: limits.maxSlotsPerGame,\n maxDataBytes: limits.maxDataBytes,\n maxBytesPerPlayer: limits.maxBytesPerPlayer,\n },\n usage: { slots: usage.slots, playerBytes: usage.playerBytes },\n });\n};\n"]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type PixelumeAuthApi, type PixelumeAuthState } from '../modules/auth/AuthModule.js';
|
|
2
2
|
import { type PixelumeHealthApi } from '../modules/health/HealthModule.js';
|
|
3
|
+
import { type PixelumeSavesApi } from '../modules/saves/SavesModule.js';
|
|
3
4
|
import type { PixelumeClientConfig, PixelumeClientOptions } from './types.js';
|
|
4
5
|
/**
|
|
5
6
|
* Entry point of the SDK. Create one instance per API/game and share it.
|
|
@@ -9,8 +10,9 @@ import type { PixelumeClientConfig, PixelumeClientOptions } from './types.js';
|
|
|
9
10
|
* const health = await pixelume.health.check();
|
|
10
11
|
* ```
|
|
11
12
|
*
|
|
12
|
-
* Feature modules are exposed as read-only properties
|
|
13
|
-
*
|
|
13
|
+
* Feature modules are exposed as read-only properties: `health`, `auth` and `saves`. New modules are
|
|
14
|
+
* added as new properties without changing existing ones. Nothing touches the DOM; the visual
|
|
15
|
+
* widget lives in the opt-in `@pixelume/sdk/ui` entry point.
|
|
14
16
|
*
|
|
15
17
|
* @throws {PixelumeConfigError} when the options are invalid.
|
|
16
18
|
*/
|
|
@@ -19,12 +21,20 @@ export declare class PixelumeClient {
|
|
|
19
21
|
readonly config: PixelumeClientConfig;
|
|
20
22
|
readonly health: PixelumeHealthApi;
|
|
21
23
|
readonly auth: PixelumeAuthApi;
|
|
24
|
+
/** Cloud Saves of the signed-in player for `gameId` (requires the `saves` scope). */
|
|
25
|
+
readonly saves: PixelumeSavesApi;
|
|
22
26
|
constructor(options?: PixelumeClientOptions);
|
|
23
27
|
/**
|
|
24
28
|
* Initializes browser authentication once. Callback failures become an anonymous state so a
|
|
25
29
|
* game can always continue as a visitor; inspect `pixelume.auth.state.error` for diagnostics.
|
|
26
30
|
*/
|
|
27
31
|
initialize(): Promise<PixelumeAuthState>;
|
|
32
|
+
/**
|
|
33
|
+
* Stops renewal timers and resume listeners and drops credentials from memory, without
|
|
34
|
+
* contacting the API. Call it when the game is torn down (SPA route change, hot reload); call
|
|
35
|
+
* `auth.logout()` first to also revoke the authorization. The client is unusable afterwards.
|
|
36
|
+
*/
|
|
37
|
+
destroy(): void;
|
|
28
38
|
}
|
|
29
39
|
/** Concise alias for new integrations; `PixelumeClient` remains supported. */
|
|
30
40
|
export { PixelumeClient as Pixelume };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { HttpClient } from '../http/HttpClient.js';
|
|
2
2
|
import { AuthModule, } from '../modules/auth/AuthModule.js';
|
|
3
3
|
import { HealthModule } from '../modules/health/HealthModule.js';
|
|
4
|
+
import { SavesModule } from '../modules/saves/SavesModule.js';
|
|
4
5
|
import { resolveConfig } from './config.js';
|
|
5
6
|
/**
|
|
6
7
|
* Entry point of the SDK. Create one instance per API/game and share it.
|
|
@@ -10,8 +11,9 @@ import { resolveConfig } from './config.js';
|
|
|
10
11
|
* const health = await pixelume.health.check();
|
|
11
12
|
* ```
|
|
12
13
|
*
|
|
13
|
-
* Feature modules are exposed as read-only properties
|
|
14
|
-
*
|
|
14
|
+
* Feature modules are exposed as read-only properties: `health`, `auth` and `saves`. New modules are
|
|
15
|
+
* added as new properties without changing existing ones. Nothing touches the DOM; the visual
|
|
16
|
+
* widget lives in the opt-in `@pixelume/sdk/ui` entry point.
|
|
15
17
|
*
|
|
16
18
|
* @throws {PixelumeConfigError} when the options are invalid.
|
|
17
19
|
*/
|
|
@@ -19,6 +21,8 @@ export class PixelumeClient {
|
|
|
19
21
|
config;
|
|
20
22
|
health;
|
|
21
23
|
auth;
|
|
24
|
+
/** Cloud Saves of the signed-in player for `gameId` (requires the `saves` scope). */
|
|
25
|
+
saves;
|
|
22
26
|
#authModule;
|
|
23
27
|
constructor(options = {}) {
|
|
24
28
|
const resolved = resolveConfig(options);
|
|
@@ -27,6 +31,7 @@ export class PixelumeClient {
|
|
|
27
31
|
gameId: resolved.gameId,
|
|
28
32
|
clientId: resolved.clientId,
|
|
29
33
|
redirectUri: resolved.redirectUri,
|
|
34
|
+
scope: resolved.scope,
|
|
30
35
|
authUrl: resolved.authUrl,
|
|
31
36
|
restoreSession: resolved.restoreSession,
|
|
32
37
|
timeoutMs: resolved.timeoutMs,
|
|
@@ -41,6 +46,7 @@ export class PixelumeClient {
|
|
|
41
46
|
this.health = new HealthModule(http);
|
|
42
47
|
this.#authModule = new AuthModule(http, resolved);
|
|
43
48
|
this.auth = this.#authModule;
|
|
49
|
+
this.saves = new SavesModule(this.#authModule.sessionManager, resolved);
|
|
44
50
|
}
|
|
45
51
|
/**
|
|
46
52
|
* Initializes browser authentication once. Callback failures become an anonymous state so a
|
|
@@ -49,6 +55,14 @@ export class PixelumeClient {
|
|
|
49
55
|
initialize() {
|
|
50
56
|
return this.#authModule.initialize();
|
|
51
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Stops renewal timers and resume listeners and drops credentials from memory, without
|
|
60
|
+
* contacting the API. Call it when the game is torn down (SPA route change, hot reload); call
|
|
61
|
+
* `auth.logout()` first to also revoke the authorization. The client is unusable afterwards.
|
|
62
|
+
*/
|
|
63
|
+
destroy() {
|
|
64
|
+
this.#authModule.dispose();
|
|
65
|
+
}
|
|
52
66
|
}
|
|
53
67
|
/** Concise alias for new integrations; `PixelumeClient` remains supported. */
|
|
54
68
|
export { PixelumeClient as Pixelume };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PixelumeClient.js","sourceRoot":"","sources":["../../src/core/PixelumeClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EACL,UAAU,GAGX,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,YAAY,EAA0B,MAAM,mCAAmC,CAAC;AACzF,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG5C
|
|
1
|
+
{"version":3,"file":"PixelumeClient.js","sourceRoot":"","sources":["../../src/core/PixelumeClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EACL,UAAU,GAGX,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,YAAY,EAA0B,MAAM,mCAAmC,CAAC;AACzF,OAAO,EAAE,WAAW,EAAyB,MAAM,iCAAiC,CAAC;AACrF,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG5C;;;;;;;;;;;;;GAaG;AACH,MAAM,OAAO,cAAc;IAChB,MAAM,CAAuB;IAC7B,MAAM,CAAoB;IAC1B,IAAI,CAAkB;IAC/B,qFAAqF;IAC5E,KAAK,CAAmB;IACxB,WAAW,CAAa;IAEjC,YAAY,UAAiC,EAAE;QAC7C,MAAM,QAAQ,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;QACxC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;YAC1B,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,QAAQ,EAAE,QAAQ,CAAC,QAAQ;YAC3B,WAAW,EAAE,QAAQ,CAAC,WAAW;YACjC,KAAK,EAAE,QAAQ,CAAC,KAAK;YACrB,OAAO,EAAE,QAAQ,CAAC,OAAO;YACzB,cAAc,EAAE,QAAQ,CAAC,cAAc;YACvC,SAAS,EAAE,QAAQ,CAAC,SAAS;YAC7B,WAAW,EAAE,QAAQ,CAAC,WAAW;SAClC,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC;YAC1B,OAAO,EAAE,QAAQ,CAAC,MAAM;YACxB,KAAK,EAAE,QAAQ,CAAC,KAAK;YACrB,SAAS,EAAE,QAAQ,CAAC,SAAS;YAC7B,WAAW,EAAE,QAAQ,CAAC,WAAW;SAClC,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,GAAG,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC;QACrC,IAAI,CAAC,WAAW,GAAG,IAAI,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QAClD,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC;QAC7B,IAAI,CAAC,KAAK,GAAG,IAAI,WAAW,CAAC,IAAI,CAAC,WAAW,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC;IAC1E,CAAC;IAED;;;OAGG;IACH,UAAU;QACR,OAAO,IAAI,CAAC,WAAW,CAAC,UAAU,EAAE,CAAC;IACvC,CAAC;IAED;;;;OAIG;IACH,OAAO;QACL,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC;IAC7B,CAAC;CACF;AAED,8EAA8E;AAC9E,OAAO,EAAE,cAAc,IAAI,QAAQ,EAAE,CAAC","sourcesContent":["import { HttpClient } from '../http/HttpClient.js';\nimport {\n AuthModule,\n type PixelumeAuthApi,\n type PixelumeAuthState,\n} from '../modules/auth/AuthModule.js';\nimport { HealthModule, type PixelumeHealthApi } from '../modules/health/HealthModule.js';\nimport { SavesModule, type PixelumeSavesApi } from '../modules/saves/SavesModule.js';\nimport { resolveConfig } from './config.js';\nimport type { PixelumeClientConfig, PixelumeClientOptions } from './types.js';\n\n/**\n * Entry point of the SDK. Create one instance per API/game and share it.\n *\n * ```ts\n * const pixelume = new PixelumeClient({ gameId: 'rail-puzzle' });\n * const health = await pixelume.health.check();\n * ```\n *\n * Feature modules are exposed as read-only properties: `health`, `auth` and `saves`. New modules are\n * added as new properties without changing existing ones. Nothing touches the DOM; the visual\n * widget lives in the opt-in `@pixelume/sdk/ui` entry point.\n *\n * @throws {PixelumeConfigError} when the options are invalid.\n */\nexport class PixelumeClient {\n readonly config: PixelumeClientConfig;\n readonly health: PixelumeHealthApi;\n readonly auth: PixelumeAuthApi;\n /** Cloud Saves of the signed-in player for `gameId` (requires the `saves` scope). */\n readonly saves: PixelumeSavesApi;\n readonly #authModule: AuthModule;\n\n constructor(options: PixelumeClientOptions = {}) {\n const resolved = resolveConfig(options);\n this.config = Object.freeze({\n apiUrl: resolved.apiUrl,\n gameId: resolved.gameId,\n clientId: resolved.clientId,\n redirectUri: resolved.redirectUri,\n scope: resolved.scope,\n authUrl: resolved.authUrl,\n restoreSession: resolved.restoreSession,\n timeoutMs: resolved.timeoutMs,\n credentials: resolved.credentials,\n });\n const http = new HttpClient({\n baseUrl: resolved.apiUrl,\n fetch: resolved.fetch,\n timeoutMs: resolved.timeoutMs,\n credentials: resolved.credentials,\n });\n this.health = new HealthModule(http);\n this.#authModule = new AuthModule(http, resolved);\n this.auth = this.#authModule;\n this.saves = new SavesModule(this.#authModule.sessionManager, resolved);\n }\n\n /**\n * Initializes browser authentication once. Callback failures become an anonymous state so a\n * game can always continue as a visitor; inspect `pixelume.auth.state.error` for diagnostics.\n */\n initialize(): Promise<PixelumeAuthState> {\n return this.#authModule.initialize();\n }\n\n /**\n * Stops renewal timers and resume listeners and drops credentials from memory, without\n * contacting the API. Call it when the game is torn down (SPA route change, hot reload); call\n * `auth.logout()` first to also revoke the authorization. The client is unusable afterwards.\n */\n destroy(): void {\n this.#authModule.dispose();\n }\n}\n\n/** Concise alias for new integrations; `PixelumeClient` remains supported. */\nexport { PixelumeClient as Pixelume };\n"]}
|