@pixelume/sdk 0.4.0 → 0.5.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 CHANGED
@@ -7,6 +7,37 @@ conter mudanças incompatíveis, sempre listadas em **Changed/Removed**.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.0] - 2026-10-14
11
+
12
+ ### Added
13
+
14
+ - `restoreAcrossSessions` (opt-in, padrão `false`): recupera a conta numa **aba nova ou após reabrir o
15
+ navegador** enquanto a sessão central da Pixelume for válida, sem pedir senha. Após um login, o SDK
16
+ guarda em `localStorage` só um indício não secreto (`pixelume.oauth.restore.<clientId>`: versão,
17
+ escopo concedido e data) e, num início sem sessão em memória, faz **no máximo uma** autorização
18
+ `prompt=none` (ida e volta de topo, sem tela). Requer a Pixelume API com `prompt=none`
19
+ (implantada em 14/10/2026). `true` usa 30 dias; `{ maxAgeDays }` aceita de 1 a 90.
20
+ - Estado `redirecting`/`exchanging` com `restoring: true` durante a tentativa silenciosa.
21
+ - Motivos anônimos `session_ended` (a API respondeu `login_required`/`consent_required`/
22
+ `interaction_required`: o indício é removido e nada é tentado de novo automaticamente) e `offline`
23
+ (sem conexão para tentar ou concluir a recuperação: o indício é mantido).
24
+ - `PixelumeAuthError` com `reason: 'restore_rejected'` quando a recuperação é recusada por outro
25
+ motivo (por exemplo, `saves` deixou de ser permitido: o indício passa a pedir só `profile`).
26
+ - `PixelumeBrowserAdapter.persistentStorage` e `isOnline()` opcionais; tipo
27
+ `PixelumeCrossSessionRestoreOptions`; `config.restoreAcrossSessions`.
28
+ - Widget: “Conectando à sua conta Pixelume…” durante a recuperação e textos para sessão encerrada e
29
+ falta de conexão.
30
+
31
+ ### Changed
32
+
33
+ - Com `restoreAcrossSessions`, o reload da mesma aba também usa `prompt=none` em vez de passar pelo
34
+ portal: se a sessão central tiver acabado, o jogador continua como visitante em vez de ver a tela de
35
+ login. Sem a opção, o comportamento é idêntico ao da 0.4.0 (um indício deixado por uma versão com a
36
+ opção ligada é apenas removido).
37
+ - Proteções contra loop: uma tentativa por início, marcador por aba que impede nova tentativa por
38
+ 10 minutos se a anterior não voltar, callback repetido recusado sem troca de código, retorno do
39
+ back/forward cache durante a tentativa vira visitante. Tokens continuam só em memória.
40
+
10
41
  ## [0.4.0] - 2026-10-14
11
42
 
12
43
  ### Added
package/README.md CHANGED
@@ -141,8 +141,11 @@ avisos de conexão durante a renovação e, apenas depois de `saves.save()`/`sav
141
141
  status da nuvem.
142
142
 
143
143
  `pixelume.auth.state.status` é `uninitialized`, `redirecting`, `exchanging`, `anonymous` ou
144
- `authenticated`. O estado anônimo informa `reason` (`visitor`, `cancelled`, `expired`, `logged_out` ou
145
- `error`) e, em falha, um `PixelumeError`. Cancelar no portal resulta em visitante, não em exceção.
144
+ `authenticated`. `redirecting`/`exchanging` trazem `restoring: true` durante a recuperação silenciosa
145
+ entre sessões. O estado anônimo informa `reason` (`visitor`, `cancelled`, `expired`, `logged_out`,
146
+ `session_ended`, `offline` ou `error`) e, em falha, um `PixelumeError`. Cancelar no portal resulta em
147
+ visitante, não em exceção. `visitor` nunca significa “a sessão central acabou”: só que não havia o que
148
+ recuperar nesta aba/navegador.
146
149
 
147
150
  ### Visitante, recarga e expiração
148
151
 
@@ -153,6 +156,7 @@ status da nuvem.
153
156
  tinha uma sessão (e o escopo concedido). Em uma recarga, inicia uma nova autorização com o mesmo
154
157
  escopo; enquanto a sessão central da API continuar válida, o portal retorna sem pedir senha.
155
158
  `restoreSession: false` desliga esse comportamento.
159
+ - Aba nova ou navegador reaberto: veja [Recuperação entre sessões](#recuperação-entre-sessões-do-navegador-opt-in).
156
160
  - PKCE `state`/`code_verifier` ficam na sessão por no máximo 10 minutos e são removidos antes da troca.
157
161
  - Código expirado/repetido, `state` divergente, callback adulterado e 401 deixam o jogo utilizável como
158
162
  visitante. O jogo pode oferecer o login novamente.
@@ -160,21 +164,59 @@ status da nuvem.
160
164
  `/v1/oauth/revoke` (o que encerra todos os tokens daquela autorização). A sessão central do portal
161
165
  continua existente por desenho; entrar novamente pode ser imediato.
162
166
 
167
+ ### Recuperação entre sessões do navegador (opt-in)
168
+
169
+ Sem a opção, uma aba nova ou o navegador reaberto começam como visitante, mesmo com a sessão central
170
+ da Pixelume ainda válida (30 dias). Com `restoreAcrossSessions: true`:
171
+
172
+ ```ts
173
+ const pixelume = new Pixelume({
174
+ gameId: 'meu-jogo',
175
+ scope: 'profile saves',
176
+ restoreAcrossSessions: true, // ou { maxAgeDays: 7 }
177
+ });
178
+ ```
179
+
180
+ 1. Depois de um login, o SDK grava em `localStorage` somente `pixelume.oauth.restore.<clientId>` =
181
+ `{ version, scope, at }` — nenhum token, código, verifier, ID ou e-mail.
182
+ 2. Num início sem sessão em memória (aba nova, navegador reaberto ou reload), se esse indício existir
183
+ e tiver menos de `maxAgeDays` (padrão 30), `initialize()` publica
184
+ `{ status: 'redirecting', restoring: true }` e faz uma navegação de topo para
185
+ `/v1/oauth/authorize?…&prompt=none`. A API **não mostra tela**: devolve o `code` (PKCE S256 + `state`)
186
+ se a sessão central for válida, ou `error=login_required`.
187
+ 3. No retorno: sucesso → `authenticated` (o escopo pedido nunca passa do anterior nem do configurado), e o
188
+ SDK devolve a URL original (caminho, query e hash) com `history.replaceState`. `login_required` →
189
+ `anonymous/session_ended`, o indício é apagado e **nenhuma** tentativa automática se repete até um novo
190
+ login manual.
191
+ 4. Sem conexão no início → `anonymous/offline`, sem navegação, indício mantido (o próximo início tenta).
192
+
193
+ **UX:** a tentativa é uma ida e volta de topo de poucas centenas de milissegundos; o jogo carrega duas
194
+ vezes. Mostre “Conectando…” enquanto `status` for `uninitialized`/`redirecting`/`exchanging` e, se o jogo
195
+ roteia pelo endereço, leia a rota depois de `initialize()` (ou guarde-a ao ver `redirecting`). Estado em
196
+ memória do jogo antes de `initialize()` se perde na navegação.
197
+
198
+ **Proteções:** uma tentativa por início; um marcador por aba (`sessionStorage`) impede nova tentativa por
199
+ 10 minutos se a anterior não voltar (erro de rede, botão Voltar, back/forward cache → `visitor`); callback
200
+ repetido é recusado sem troca de código; cada aba usa seu próprio `state`/verifier; `logout()` em qualquer
201
+ aba apaga o indício. Sem `localStorage` disponível (bloqueado, adaptador injetado sem
202
+ `persistentStorage`), o comportamento é o da 0.4. Com a opção desligada, um indício antigo é removido.
203
+
163
204
  ## Configuração
164
205
 
165
- | Opção | Padrão | Observação |
166
- | ---------------- | ------------------------------------ | ----------------------------------------------------------------------------------------- |
167
- | `apiUrl` | `https://api.pixelume.com.br` | HTTPS; HTTP só em loopback ou com `allowInsecureHttp` explícito |
168
- | `authUrl` | `https://auth.pixelume.com.br` | Portal esperado; a API continua sendo a autoridade que escolhe o redirect |
169
- | `gameId` | — | slug público do jogo; também é o `clientId` padrão |
170
- | `clientId` | `gameId` | identificador OAuth público cadastrado; nunca é credencial |
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`) |
173
- | `restoreSession` | `true` | reautoriza após reload da mesma aba sem persistir token |
174
- | `timeoutMs` | `10000` | cobre resposta e leitura do corpo |
175
- | `credentials` | `omit` | política dos módulos genéricos; Auth força `omit` para token/userinfo/revoke |
176
- | `fetch` | `globalThis.fetch` | injetável para testes/instrumentação |
177
- | `browser` | globals do browser | adaptador avançado para webviews e testes |
206
+ | Opção | Padrão | Observação |
207
+ | ----------------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
208
+ | `apiUrl` | `https://api.pixelume.com.br` | HTTPS; HTTP só em loopback ou com `allowInsecureHttp` explícito |
209
+ | `authUrl` | `https://auth.pixelume.com.br` | Portal esperado; a API continua sendo a autoridade que escolhe o redirect |
210
+ | `gameId` | — | slug público do jogo; também é o `clientId` padrão |
211
+ | `clientId` | `gameId` | identificador OAuth público cadastrado; nunca é credencial |
212
+ | `redirectUri` | `<origin>/oauth/callback` no browser | deve coincidir exatamente com o cadastro e não pode conter fragmento nem parâmetros OAuth |
213
+ | `scope` | `profile` | `profile saves` para Cloud Saves (o cadastro do jogo precisa permitir `saves`) |
214
+ | `restoreSession` | `true` | reautoriza após reload da mesma aba sem persistir token |
215
+ | `restoreAcrossSessions` | `false` | opt-in: recupera a conta em aba nova/navegador reaberto via `prompt=none` (indício não secreto em `localStorage`) |
216
+ | `timeoutMs` | `10000` | cobre resposta e leitura do corpo |
217
+ | `credentials` | `omit` | política dos módulos genéricos; Auth força `omit` para token/userinfo/revoke |
218
+ | `fetch` | `globalThis.fetch` | injetável para testes/instrumentação |
219
+ | `browser` | globals do browser | adaptador avançado para webviews e testes |
178
220
 
179
221
  `authUrl` é informativo/configurável para ambientes, mas o SDK nunca concatena nem escolhe um retorno
180
222
  via portal: `/v1/oauth/authorize` valida o cliente/callback e redireciona para o portal configurado na
@@ -202,7 +244,8 @@ uma ferramenta de teste, não UI oficial.
202
244
  - `crypto.getRandomValues` e Web Crypto SHA-256 geram `state`, verifier e challenge; não há
203
245
  `Math.random`.
204
246
  - 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.
247
+ `sessionStorage`, cookies, URL, estado observável ou mensagens de erro. Com `restoreAcrossSessions`,
248
+ `localStorage` recebe apenas o indício `{ version, scope, at }`.
206
249
  - CORS/Origin não são considerados autenticação. PKCE, URI exata, state e vínculos persistidos pela API
207
250
  são os controles de segurança.
208
251
  - XSS ainda pode ler um token que esteja em memória; jogos devem manter CSP e dependências sob controle.
@@ -34,6 +34,7 @@ export class PixelumeClient {
34
34
  scope: resolved.scope,
35
35
  authUrl: resolved.authUrl,
36
36
  restoreSession: resolved.restoreSession,
37
+ restoreAcrossSessions: resolved.crossSessionRestore !== null,
37
38
  timeoutMs: resolved.timeoutMs,
38
39
  credentials: resolved.credentials,
39
40
  });
@@ -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,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"]}
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,qBAAqB,EAAE,QAAQ,CAAC,mBAAmB,KAAK,IAAI;YAC5D,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 restoreAcrossSessions: resolved.crossSessionRestore !== null,\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"]}
@@ -11,6 +11,10 @@ export interface ResolvedConfig {
11
11
  scope: 'profile' | 'profile saves';
12
12
  authUrl: string;
13
13
  restoreSession: boolean;
14
+ /** `null` when cross-session restoration is disabled. */
15
+ crossSessionRestore: {
16
+ readonly maxAgeMs: number;
17
+ } | null;
14
18
  allowInsecureHttp: boolean;
15
19
  browser: PixelumeClientOptions['browser'];
16
20
  timeoutMs: number;
@@ -17,6 +17,7 @@ export function resolveConfig(options) {
17
17
  redirectUri: resolveRedirectUri(options, allowInsecureHttp),
18
18
  scope: validateScope(options.scope ?? 'profile'),
19
19
  restoreSession: validateBoolean('restoreSession', options.restoreSession ?? true),
20
+ crossSessionRestore: resolveCrossSessionRestore(options.restoreAcrossSessions),
20
21
  allowInsecureHttp,
21
22
  browser: options.browser,
22
23
  timeoutMs: options.timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : validateTimeout(options.timeoutMs),
@@ -34,6 +35,23 @@ function validateScope(value) {
34
35
  }
35
36
  return value;
36
37
  }
38
+ const DAY_MS = 24 * 60 * 60_000;
39
+ function resolveCrossSessionRestore(value) {
40
+ if (value === undefined || value === false)
41
+ return null;
42
+ if (value === true)
43
+ return Object.freeze({ maxAgeMs: 30 * DAY_MS });
44
+ // Validated at runtime: JavaScript callers may pass anything.
45
+ const raw = value;
46
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
47
+ throw new PixelumeConfigError('restoreAcrossSessions must be a boolean or { maxAgeDays }');
48
+ }
49
+ const days = raw.maxAgeDays ?? 30;
50
+ if (typeof days !== 'number' || !Number.isInteger(days) || days < 1 || days > 90) {
51
+ throw new PixelumeConfigError('restoreAcrossSessions.maxAgeDays must be an integer from 1 to 90');
52
+ }
53
+ return Object.freeze({ maxAgeMs: days * DAY_MS });
54
+ }
37
55
  function validateBoolean(name, value) {
38
56
  if (typeof value !== 'boolean')
39
57
  throw new PixelumeConfigError(`${name} must be a boolean`);
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CredentialsPolicy, FetchLike } from '../http/types.js';\nimport { PixelumeConfigError } from './errors.js';\nimport type { PixelumeClientOptions } from './types.js';\n\nexport const DEFAULT_API_URL = 'https://api.pixelume.com.br';\nexport const DEFAULT_AUTH_URL = 'https://auth.pixelume.com.br';\nexport const DEFAULT_TIMEOUT_MS = 10_000;\nconst MAX_TIMEOUT_MS = 2_147_483_647; // setTimeout limit\nconst GAME_ID_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/u;\nconst CLIENT_ID_PATTERN = /^[A-Za-z0-9._-]{1,128}$/u;\nconst CREDENTIALS: readonly CredentialsPolicy[] = ['omit', 'same-origin', 'include'];\n\nexport interface ResolvedConfig {\n apiUrl: string;\n gameId: string | null;\n clientId: string | null;\n redirectUri: string | null;\n scope: 'profile' | 'profile saves';\n authUrl: string;\n restoreSession: boolean;\n allowInsecureHttp: boolean;\n browser: PixelumeClientOptions['browser'];\n timeoutMs: number;\n credentials: CredentialsPolicy;\n fetch: FetchLike;\n}\n\nexport function resolveConfig(options: PixelumeClientOptions): ResolvedConfig {\n const allowInsecureHttp = options.allowInsecureHttp === true;\n const gameId = options.gameId === undefined ? null : validateGameId(options.gameId);\n return {\n apiUrl: normalizeApiUrl(options.apiUrl ?? DEFAULT_API_URL, allowInsecureHttp),\n authUrl: normalizeOrigin(options.authUrl ?? DEFAULT_AUTH_URL, 'authUrl', allowInsecureHttp),\n gameId,\n clientId: options.clientId === undefined ? gameId : validateClientId(options.clientId),\n redirectUri: resolveRedirectUri(options, allowInsecureHttp),\n scope: validateScope(options.scope ?? 'profile'),\n restoreSession: validateBoolean('restoreSession', options.restoreSession ?? true),\n allowInsecureHttp,\n browser: options.browser,\n timeoutMs:\n options.timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : validateTimeout(options.timeoutMs),\n credentials: validateCredentials(options.credentials ?? 'omit'),\n fetch: resolveFetch(options.fetch),\n };\n}\n\nfunction resolveRedirectUri(\n options: PixelumeClientOptions,\n allowInsecureHttp: boolean,\n): string | null {\n const value = options.redirectUri ?? defaultRedirectUri(options.browser);\n return value === null ? null : normalizeRedirectUri(value, allowInsecureHttp);\n}\n\nfunction validateScope(value: unknown): 'profile' | 'profile saves' {\n if (value !== 'profile' && value !== 'profile saves') {\n throw new PixelumeConfigError('scope must be \"profile\" or \"profile saves\"');\n }\n return value;\n}\n\nfunction validateBoolean(name: string, value: boolean): boolean {\n if (typeof value !== 'boolean') throw new PixelumeConfigError(`${name} must be a boolean`);\n return value;\n}\n\nfunction defaultRedirectUri(browser: PixelumeClientOptions['browser']): string | null {\n const location = Reflect.get(globalThis, 'location') as unknown;\n const current =\n browser?.currentUrl() ??\n (typeof location === 'object' && location !== null && 'href' in location\n ? String(location.href)\n : null);\n if (!current) return null;\n const url = new URL(current);\n return `${url.origin}/oauth/callback`;\n}\n\n/**\n * Accepts `https:` URLs, plus `http:` for loopback hosts (`localhost`, `*.localhost`, `127.0.0.0/8`,\n * `[::1]`). Other `http:` hosts (e.g. a LAN IP while testing on a phone) need `allowInsecureHttp`.\n */\nexport function normalizeApiUrl(value: string, allowInsecureHttp: boolean): string {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n throw new PixelumeConfigError(\n 'apiUrl must be an absolute URL, e.g. https://api.pixelume.com.br',\n );\n }\n if (url.protocol !== 'https:' && url.protocol !== 'http:') {\n throw new PixelumeConfigError('apiUrl must use https: (or http: for local development)');\n }\n if (url.protocol === 'http:' && !allowInsecureHttp && !isLoopbackHost(url.hostname)) {\n throw new PixelumeConfigError(\n 'apiUrl must use https:. Plain http: is only accepted for localhost unless allowInsecureHttp is set',\n );\n }\n if (url.username || url.password) {\n throw new PixelumeConfigError('apiUrl must not contain credentials');\n }\n if (url.search || url.hash) {\n throw new PixelumeConfigError('apiUrl must not contain a query string or fragment');\n }\n return `${url.origin}${url.pathname.replace(/\\/+$/u, '')}`;\n}\n\nfunction normalizeOrigin(value: string, name: string, allowInsecureHttp: boolean): string {\n const normalized = normalizeApiUrl(value, allowInsecureHttp);\n const url = new URL(normalized);\n if (url.pathname !== '/') {\n throw new PixelumeConfigError(`${name} must be an origin without a path`);\n }\n return url.origin;\n}\n\nexport function normalizeRedirectUri(value: string, allowInsecureHttp: boolean): string {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n throw new PixelumeConfigError('redirectUri must be an absolute URL registered by the API');\n }\n if (url.protocol !== 'https:' && url.protocol !== 'http:') {\n throw new PixelumeConfigError('redirectUri must use https: (or http: for local development)');\n }\n if (url.protocol === 'http:' && !allowInsecureHttp && !isLoopbackHost(url.hostname)) {\n throw new PixelumeConfigError('redirectUri must use https: outside localhost');\n }\n if (url.username || url.password || url.hash) {\n throw new PixelumeConfigError('redirectUri must not contain credentials or a fragment');\n }\n for (const key of ['code', 'state', 'error', 'error_description']) {\n if (url.searchParams.has(key)) {\n throw new PixelumeConfigError(\n `redirectUri must not contain the reserved query parameter \"${key}\"`,\n );\n }\n }\n return url.toString();\n}\n\nfunction isLoopbackHost(hostname: string): boolean {\n return (\n hostname === 'localhost' ||\n hostname.endsWith('.localhost') ||\n hostname === '[::1]' ||\n /^127(?:\\.\\d{1,3}){3}$/u.test(hostname)\n );\n}\n\n/**\n * Game identifiers are public, non-secret slugs (e.g. `rail-puzzle`): lowercase letters, digits\n * and inner hyphens, 1–64 characters. They identify a game, they never authorize anything.\n */\nexport function validateGameId(value: string): string {\n if (typeof value !== 'string' || !GAME_ID_PATTERN.test(value)) {\n throw new PixelumeConfigError(\n 'gameId must be a lowercase slug (a-z, 0-9 and inner hyphens, up to 64 characters)',\n );\n }\n return value;\n}\n\nexport function validateClientId(value: string): string {\n if (typeof value !== 'string' || !CLIENT_ID_PATTERN.test(value)) {\n throw new PixelumeConfigError(\n 'clientId must contain 1-128 letters, digits, dots, underscores or hyphens',\n );\n }\n return value;\n}\n\n/** For feature modules that need a game context (Cloud Saves). */\nexport function requireGameId(config: Pick<ResolvedConfig, 'gameId'>, feature: string): string {\n if (config.gameId === null) {\n throw new PixelumeConfigError(`${feature} requires the client to be created with a gameId`);\n }\n return config.gameId;\n}\n\nexport function validateTimeout(value: number): number {\n if (!Number.isInteger(value) || value <= 0 || value > MAX_TIMEOUT_MS) {\n throw new PixelumeConfigError('timeoutMs must be a positive integer number of milliseconds');\n }\n return value;\n}\n\nfunction validateCredentials(value: CredentialsPolicy): CredentialsPolicy {\n if (!CREDENTIALS.includes(value)) {\n throw new PixelumeConfigError(`credentials must be one of: ${CREDENTIALS.join(', ')}`);\n }\n return value;\n}\n\nfunction resolveFetch(custom: FetchLike | undefined): FetchLike {\n if (custom !== undefined) {\n if (typeof custom !== 'function') throw new PixelumeConfigError('fetch must be a function');\n return custom;\n }\n if (typeof globalThis.fetch !== 'function') {\n throw new PixelumeConfigError('No global fetch available; pass the fetch option');\n }\n // Looked up on each call so polyfills or test doubles installed later are honoured.\n return (input, init) => globalThis.fetch(input, init);\n}\n"]}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CredentialsPolicy, FetchLike } from '../http/types.js';\nimport { PixelumeConfigError } from './errors.js';\nimport type { PixelumeClientOptions } from './types.js';\n\nexport const DEFAULT_API_URL = 'https://api.pixelume.com.br';\nexport const DEFAULT_AUTH_URL = 'https://auth.pixelume.com.br';\nexport const DEFAULT_TIMEOUT_MS = 10_000;\nconst MAX_TIMEOUT_MS = 2_147_483_647; // setTimeout limit\nconst GAME_ID_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/u;\nconst CLIENT_ID_PATTERN = /^[A-Za-z0-9._-]{1,128}$/u;\nconst CREDENTIALS: readonly CredentialsPolicy[] = ['omit', 'same-origin', 'include'];\n\nexport interface ResolvedConfig {\n apiUrl: string;\n gameId: string | null;\n clientId: string | null;\n redirectUri: string | null;\n scope: 'profile' | 'profile saves';\n authUrl: string;\n restoreSession: boolean;\n /** `null` when cross-session restoration is disabled. */\n crossSessionRestore: { readonly maxAgeMs: number } | null;\n allowInsecureHttp: boolean;\n browser: PixelumeClientOptions['browser'];\n timeoutMs: number;\n credentials: CredentialsPolicy;\n fetch: FetchLike;\n}\n\nexport function resolveConfig(options: PixelumeClientOptions): ResolvedConfig {\n const allowInsecureHttp = options.allowInsecureHttp === true;\n const gameId = options.gameId === undefined ? null : validateGameId(options.gameId);\n return {\n apiUrl: normalizeApiUrl(options.apiUrl ?? DEFAULT_API_URL, allowInsecureHttp),\n authUrl: normalizeOrigin(options.authUrl ?? DEFAULT_AUTH_URL, 'authUrl', allowInsecureHttp),\n gameId,\n clientId: options.clientId === undefined ? gameId : validateClientId(options.clientId),\n redirectUri: resolveRedirectUri(options, allowInsecureHttp),\n scope: validateScope(options.scope ?? 'profile'),\n restoreSession: validateBoolean('restoreSession', options.restoreSession ?? true),\n crossSessionRestore: resolveCrossSessionRestore(options.restoreAcrossSessions),\n allowInsecureHttp,\n browser: options.browser,\n timeoutMs:\n options.timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : validateTimeout(options.timeoutMs),\n credentials: validateCredentials(options.credentials ?? 'omit'),\n fetch: resolveFetch(options.fetch),\n };\n}\n\nfunction resolveRedirectUri(\n options: PixelumeClientOptions,\n allowInsecureHttp: boolean,\n): string | null {\n const value = options.redirectUri ?? defaultRedirectUri(options.browser);\n return value === null ? null : normalizeRedirectUri(value, allowInsecureHttp);\n}\n\nfunction validateScope(value: unknown): 'profile' | 'profile saves' {\n if (value !== 'profile' && value !== 'profile saves') {\n throw new PixelumeConfigError('scope must be \"profile\" or \"profile saves\"');\n }\n return value;\n}\n\nconst DAY_MS = 24 * 60 * 60_000;\n\nfunction resolveCrossSessionRestore(\n value: PixelumeClientOptions['restoreAcrossSessions'],\n): { readonly maxAgeMs: number } | null {\n if (value === undefined || value === false) return null;\n if (value === true) return Object.freeze({ maxAgeMs: 30 * DAY_MS });\n // Validated at runtime: JavaScript callers may pass anything.\n const raw: unknown = value;\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {\n throw new PixelumeConfigError('restoreAcrossSessions must be a boolean or { maxAgeDays }');\n }\n const days: unknown = (raw as { maxAgeDays?: unknown }).maxAgeDays ?? 30;\n if (typeof days !== 'number' || !Number.isInteger(days) || days < 1 || days > 90) {\n throw new PixelumeConfigError(\n 'restoreAcrossSessions.maxAgeDays must be an integer from 1 to 90',\n );\n }\n return Object.freeze({ maxAgeMs: days * DAY_MS });\n}\n\nfunction validateBoolean(name: string, value: boolean): boolean {\n if (typeof value !== 'boolean') throw new PixelumeConfigError(`${name} must be a boolean`);\n return value;\n}\n\nfunction defaultRedirectUri(browser: PixelumeClientOptions['browser']): string | null {\n const location = Reflect.get(globalThis, 'location') as unknown;\n const current =\n browser?.currentUrl() ??\n (typeof location === 'object' && location !== null && 'href' in location\n ? String(location.href)\n : null);\n if (!current) return null;\n const url = new URL(current);\n return `${url.origin}/oauth/callback`;\n}\n\n/**\n * Accepts `https:` URLs, plus `http:` for loopback hosts (`localhost`, `*.localhost`, `127.0.0.0/8`,\n * `[::1]`). Other `http:` hosts (e.g. a LAN IP while testing on a phone) need `allowInsecureHttp`.\n */\nexport function normalizeApiUrl(value: string, allowInsecureHttp: boolean): string {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n throw new PixelumeConfigError(\n 'apiUrl must be an absolute URL, e.g. https://api.pixelume.com.br',\n );\n }\n if (url.protocol !== 'https:' && url.protocol !== 'http:') {\n throw new PixelumeConfigError('apiUrl must use https: (or http: for local development)');\n }\n if (url.protocol === 'http:' && !allowInsecureHttp && !isLoopbackHost(url.hostname)) {\n throw new PixelumeConfigError(\n 'apiUrl must use https:. Plain http: is only accepted for localhost unless allowInsecureHttp is set',\n );\n }\n if (url.username || url.password) {\n throw new PixelumeConfigError('apiUrl must not contain credentials');\n }\n if (url.search || url.hash) {\n throw new PixelumeConfigError('apiUrl must not contain a query string or fragment');\n }\n return `${url.origin}${url.pathname.replace(/\\/+$/u, '')}`;\n}\n\nfunction normalizeOrigin(value: string, name: string, allowInsecureHttp: boolean): string {\n const normalized = normalizeApiUrl(value, allowInsecureHttp);\n const url = new URL(normalized);\n if (url.pathname !== '/') {\n throw new PixelumeConfigError(`${name} must be an origin without a path`);\n }\n return url.origin;\n}\n\nexport function normalizeRedirectUri(value: string, allowInsecureHttp: boolean): string {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n throw new PixelumeConfigError('redirectUri must be an absolute URL registered by the API');\n }\n if (url.protocol !== 'https:' && url.protocol !== 'http:') {\n throw new PixelumeConfigError('redirectUri must use https: (or http: for local development)');\n }\n if (url.protocol === 'http:' && !allowInsecureHttp && !isLoopbackHost(url.hostname)) {\n throw new PixelumeConfigError('redirectUri must use https: outside localhost');\n }\n if (url.username || url.password || url.hash) {\n throw new PixelumeConfigError('redirectUri must not contain credentials or a fragment');\n }\n for (const key of ['code', 'state', 'error', 'error_description']) {\n if (url.searchParams.has(key)) {\n throw new PixelumeConfigError(\n `redirectUri must not contain the reserved query parameter \"${key}\"`,\n );\n }\n }\n return url.toString();\n}\n\nfunction isLoopbackHost(hostname: string): boolean {\n return (\n hostname === 'localhost' ||\n hostname.endsWith('.localhost') ||\n hostname === '[::1]' ||\n /^127(?:\\.\\d{1,3}){3}$/u.test(hostname)\n );\n}\n\n/**\n * Game identifiers are public, non-secret slugs (e.g. `rail-puzzle`): lowercase letters, digits\n * and inner hyphens, 1–64 characters. They identify a game, they never authorize anything.\n */\nexport function validateGameId(value: string): string {\n if (typeof value !== 'string' || !GAME_ID_PATTERN.test(value)) {\n throw new PixelumeConfigError(\n 'gameId must be a lowercase slug (a-z, 0-9 and inner hyphens, up to 64 characters)',\n );\n }\n return value;\n}\n\nexport function validateClientId(value: string): string {\n if (typeof value !== 'string' || !CLIENT_ID_PATTERN.test(value)) {\n throw new PixelumeConfigError(\n 'clientId must contain 1-128 letters, digits, dots, underscores or hyphens',\n );\n }\n return value;\n}\n\n/** For feature modules that need a game context (Cloud Saves). */\nexport function requireGameId(config: Pick<ResolvedConfig, 'gameId'>, feature: string): string {\n if (config.gameId === null) {\n throw new PixelumeConfigError(`${feature} requires the client to be created with a gameId`);\n }\n return config.gameId;\n}\n\nexport function validateTimeout(value: number): number {\n if (!Number.isInteger(value) || value <= 0 || value > MAX_TIMEOUT_MS) {\n throw new PixelumeConfigError('timeoutMs must be a positive integer number of milliseconds');\n }\n return value;\n}\n\nfunction validateCredentials(value: CredentialsPolicy): CredentialsPolicy {\n if (!CREDENTIALS.includes(value)) {\n throw new PixelumeConfigError(`credentials must be one of: ${CREDENTIALS.join(', ')}`);\n }\n return value;\n}\n\nfunction resolveFetch(custom: FetchLike | undefined): FetchLike {\n if (custom !== undefined) {\n if (typeof custom !== 'function') throw new PixelumeConfigError('fetch must be a function');\n return custom;\n }\n if (typeof globalThis.fetch !== 'function') {\n throw new PixelumeConfigError('No global fetch available; pass the fetch option');\n }\n // Looked up on each call so polyfills or test doubles installed later are honoured.\n return (input, init) => globalThis.fetch(input, init);\n}\n"]}
@@ -14,6 +14,11 @@ export type PixelumeAuthErrorReason = 'browser_unavailable' | 'storage_unavailab
14
14
  | 'session_expired'
15
15
  /** The API rejected renewal because the game is disabled or unknown (`invalid_client`). */
16
16
  | 'game_unavailable'
17
+ /**
18
+ * A cross-session restoration attempt (`restoreAcrossSessions`) was refused for a reason other
19
+ * than an ended central session, e.g. the remembered scope is no longer allowed for the game.
20
+ */
21
+ | 'restore_rejected'
17
22
  /**
18
23
  * Renewal is not possible: the API rejected it for a non-recoverable reason (scope, origin,
19
24
  * request) or the session has no refresh token (legacy authorization).
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Error hierarchy of the SDK.\n *\n * Every error thrown by the SDK is a {@link PixelumeError} with a stable `kind` discriminator.\n * Messages are deterministic and never include request/response bodies, headers, cookies or\n * query strings, so they are safe to log. Raw response payloads are kept on\n * {@link PixelumeHttpError.payload} as a non-enumerable property for programmatic inspection only.\n */\n\nconst PIXELUME_ERROR_BRAND = Symbol.for('pixelume.sdk.error');\n\nexport type PixelumeErrorKind =\n 'config' | 'auth' | 'http' | 'network' | 'timeout' | 'aborted' | 'invalid_response';\n\nexport type PixelumeAuthErrorReason =\n | 'browser_unavailable'\n | 'storage_unavailable'\n | 'crypto_unavailable'\n | 'invalid_callback'\n | 'state_mismatch'\n | 'authorization_expired'\n /** An operation needs a signed-in player and there is none. No request was sent. */\n | 'not_authenticated'\n /** The session ended and cannot be renewed (expired, revoked, logged out elsewhere). */\n | 'session_expired'\n /** The API rejected renewal because the game is disabled or unknown (`invalid_client`). */\n | 'game_unavailable'\n /**\n * Renewal is not possible: the API rejected it for a non-recoverable reason (scope, origin,\n * request) or the session has no refresh token (legacy authorization).\n */\n | 'renewal_rejected';\n\n/**\n * Error codes documented in the Pixelume API OpenAPI `ErrorEnvelope`.\n * The API may introduce new codes, so any string is accepted at runtime.\n */\nexport type PixelumeApiErrorCode =\n | 'INVALID_INPUT'\n | 'UNAUTHENTICATED'\n | 'FORBIDDEN_ORIGIN'\n | 'FORBIDDEN_HOST'\n | 'RATE_LIMITED'\n | 'REGISTRATION_DISABLED'\n | 'AUTH_TOKEN_INVALID'\n | 'OAUTH_INVALID_CLIENT'\n | 'OAUTH_INVALID_REDIRECT_URI'\n | 'OAUTH_INVALID_REQUEST'\n | 'OAUTH_INVALID_SCOPE'\n | 'OAUTH_REQUEST_EXPIRED'\n | 'OAUTH_INVALID_GRANT'\n | 'OAUTH_INVALID_TOKEN'\n | 'OAUTH_UNSUPPORTED_GRANT_TYPE'\n | 'OAUTH_INSUFFICIENT_SCOPE'\n | 'FORBIDDEN_GAME'\n | 'GAME_INACTIVE'\n | 'SAVE_NOT_FOUND'\n | 'SAVE_REVISION_CONFLICT'\n | 'SAVE_TOO_LARGE'\n | 'SAVE_QUOTA_EXCEEDED'\n | 'INTERNAL_ERROR'\n | 'NOT_FOUND'\n | (string & {});\n\n/** Identifies the request that failed. `path` never contains the query string. */\nexport interface PixelumeRequestInfo {\n readonly method: string;\n readonly path: string;\n}\n\n/** Base class of every error thrown by the SDK. */\nexport abstract class PixelumeError extends Error {\n abstract readonly kind: PixelumeErrorKind;\n\n protected constructor(name: string, message: string, options?: { cause?: unknown }) {\n super(message, options?.cause === undefined ? undefined : { cause: options.cause });\n // Explicit names survive minification of consumer bundles.\n this.name = name;\n Object.defineProperty(this, PIXELUME_ERROR_BRAND, { value: true });\n }\n}\n\n/** Invalid SDK configuration or invalid use of the SDK. Never caused by the network. */\nexport class PixelumeConfigError extends PixelumeError {\n readonly kind = 'config' as const;\n\n constructor(message: string) {\n super('PixelumeConfigError', message);\n }\n}\n\n/** A local browser authorization failure that happened before an API response was available. */\nexport class PixelumeAuthError extends PixelumeError {\n readonly kind = 'auth' as const;\n readonly reason: PixelumeAuthErrorReason;\n\n constructor(reason: PixelumeAuthErrorReason, options?: { cause?: unknown }) {\n super('PixelumeAuthError', `Pixelume authorization failed (${reason})`, options);\n this.reason = reason;\n }\n}\n\n/** Base class of errors tied to a specific HTTP request. */\nexport abstract class PixelumeRequestError extends PixelumeError {\n readonly method: string;\n readonly path: string;\n\n protected constructor(\n name: string,\n message: string,\n request: PixelumeRequestInfo,\n options?: { cause?: unknown },\n ) {\n super(name, message, options);\n this.method = request.method;\n this.path = request.path;\n }\n}\n\n/** The API answered with a non-2xx status. */\nexport class PixelumeHttpError extends PixelumeRequestError {\n readonly kind = 'http' as const;\n readonly status: number;\n /** `error.code` from the API error envelope, or `null` when the body is not an envelope. */\n readonly code: PixelumeApiErrorCode | null;\n /** `error.message` from the API error envelope (already sanitized by the API). */\n readonly apiMessage: string | null;\n /** Request ID from the envelope or the `x-request-id` header, useful for support. */\n readonly requestId: string | null;\n /**\n * `error.details` from the API envelope (e.g. `oauthError`, `quota`, conflict metadata), or\n * `null`. The API only exposes non-sensitive details here.\n */\n readonly details: Readonly<Record<string, unknown>> | null;\n /**\n * Decoded response body (JSON value, text or `undefined`). Non-enumerable so that\n * `JSON.stringify(error)` and structured loggers do not dump it by accident.\n */\n declare readonly payload: unknown;\n\n constructor(details: {\n request: PixelumeRequestInfo;\n status: number;\n code: string | null;\n apiMessage: string | null;\n requestId: string | null;\n details?: Readonly<Record<string, unknown>> | null;\n payload: unknown;\n }) {\n const suffix = details.code ? ` (${details.code})` : '';\n super(\n 'PixelumeHttpError',\n `Pixelume API request ${details.request.method} ${details.request.path} failed with status ${details.status}${suffix}`,\n details.request,\n );\n this.status = details.status;\n this.code = details.code;\n this.apiMessage = details.apiMessage;\n this.requestId = details.requestId;\n this.details = details.details ?? null;\n Object.defineProperty(this, 'payload', {\n value: details.payload,\n enumerable: false,\n writable: false,\n });\n }\n}\n\n/** Server-side metadata of a cloud save, as reported by the API. */\nexport interface PixelumeSaveMetadata {\n readonly gameId: string;\n readonly slotId: string;\n /** Version of the save format, chosen by the game (1–1 000 000). */\n readonly schemaVersion: number;\n /** Monotonic revision assigned by the server; never restarts, even after a delete. */\n readonly revision: number;\n /** UTF-8 size of the canonical JSON of `data`. */\n readonly sizeBytes: number;\n /** ISO 8601 timestamps assigned by the server. */\n readonly createdAt: string;\n readonly updatedAt: string;\n}\n\n/**\n * `409 SAVE_REVISION_CONFLICT`: the slot changed since `expectedRevision` (another device, tab or a\n * write whose response was lost). The SDK never retries or overwrites; the game decides.\n */\nexport class PixelumeSaveConflictError extends PixelumeHttpError {\n /** Current remote metadata, or `null` when the slot no longer has a live save. */\n readonly current: PixelumeSaveMetadata | null;\n /** The revision the rejected operation expected. */\n readonly expectedRevision: number;\n\n constructor(\n error: PixelumeHttpError,\n current: PixelumeSaveMetadata | null,\n expectedRevision: number,\n ) {\n super({\n request: { method: error.method, path: error.path },\n status: error.status,\n code: error.code,\n apiMessage: error.apiMessage,\n requestId: error.requestId,\n details: error.details,\n payload: error.payload,\n });\n this.name = 'PixelumeSaveConflictError';\n this.current = current;\n this.expectedRevision = expectedRevision;\n }\n}\n\n/**\n * The request did not produce an HTTP response: offline, DNS/TLS failure, connection reset,\n * or a browser CORS rejection (browsers do not distinguish these for security reasons).\n */\nexport class PixelumeNetworkError extends PixelumeRequestError {\n readonly kind = 'network' as const;\n\n constructor(request: PixelumeRequestInfo, options?: { cause?: unknown }) {\n super(\n 'PixelumeNetworkError',\n `Pixelume API request ${request.method} ${request.path} failed before a response was received (network failure or blocked by CORS)`,\n request,\n options,\n );\n }\n}\n\n/** The SDK timeout elapsed before the response (including its body) was received. */\nexport class PixelumeTimeoutError extends PixelumeRequestError {\n readonly kind = 'timeout' as const;\n readonly timeoutMs: number;\n\n constructor(request: PixelumeRequestInfo, timeoutMs: number, options?: { cause?: unknown }) {\n super(\n 'PixelumeTimeoutError',\n `Pixelume API request ${request.method} ${request.path} timed out after ${timeoutMs} ms`,\n request,\n options,\n );\n this.timeoutMs = timeoutMs;\n }\n}\n\n/** The caller aborted the request through its `AbortSignal`. `cause` holds the abort reason. */\nexport class PixelumeAbortError extends PixelumeRequestError {\n readonly kind = 'aborted' as const;\n\n constructor(request: PixelumeRequestInfo, options?: { cause?: unknown }) {\n super(\n 'PixelumeAbortError',\n `Pixelume API request ${request.method} ${request.path} was aborted by the caller`,\n request,\n options,\n );\n }\n}\n\n/** A 2xx response did not match the contract expected by the SDK. */\nexport class PixelumeInvalidResponseError extends PixelumeRequestError {\n readonly kind = 'invalid_response' as const;\n readonly status: number;\n /** Short, payload-free description of the contract violation. */\n readonly reason: string;\n\n constructor(request: PixelumeRequestInfo, status: number, reason: string) {\n super(\n 'PixelumeInvalidResponseError',\n `Pixelume API request ${request.method} ${request.path} returned an unexpected response: ${reason}`,\n request,\n );\n this.status = status;\n this.reason = reason;\n }\n}\n\n/**\n * Returns `true` for any error thrown by the SDK. Works across duplicated copies of the package\n * (where `instanceof` would fail) because it checks a global symbol brand.\n */\nexport function isPixelumeError(value: unknown): value is PixelumeError {\n return (\n value instanceof Error &&\n (value as unknown as Record<symbol, unknown>)[PIXELUME_ERROR_BRAND] === true\n );\n}\n"]}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,oBAAoB,GAAG,MAAM,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;AAkE9D,mDAAmD;AACnD,MAAM,OAAgB,aAAc,SAAQ,KAAK;IAG/C,YAAsB,IAAY,EAAE,OAAe,EAAE,OAA6B;QAChF,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;QACpF,2DAA2D;QAC3D,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,oBAAoB,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACrE,CAAC;CACF;AAED,wFAAwF;AACxF,MAAM,OAAO,mBAAoB,SAAQ,aAAa;IAC3C,IAAI,GAAG,QAAiB,CAAC;IAElC,YAAY,OAAe;QACzB,KAAK,CAAC,qBAAqB,EAAE,OAAO,CAAC,CAAC;IACxC,CAAC;CACF;AAED,gGAAgG;AAChG,MAAM,OAAO,iBAAkB,SAAQ,aAAa;IACzC,IAAI,GAAG,MAAe,CAAC;IACvB,MAAM,CAA0B;IAEzC,YAAY,MAA+B,EAAE,OAA6B;QACxE,KAAK,CAAC,mBAAmB,EAAE,kCAAkC,MAAM,GAAG,EAAE,OAAO,CAAC,CAAC;QACjF,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAED,4DAA4D;AAC5D,MAAM,OAAgB,oBAAqB,SAAQ,aAAa;IACrD,MAAM,CAAS;IACf,IAAI,CAAS;IAEtB,YACE,IAAY,EACZ,OAAe,EACf,OAA4B,EAC5B,OAA6B;QAE7B,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;QAC9B,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAC3B,CAAC;CACF;AAED,8CAA8C;AAC9C,MAAM,OAAO,iBAAkB,SAAQ,oBAAoB;IAChD,IAAI,GAAG,MAAe,CAAC;IACvB,MAAM,CAAS;IACxB,4FAA4F;IACnF,IAAI,CAA8B;IAC3C,kFAAkF;IACzE,UAAU,CAAgB;IACnC,qFAAqF;IAC5E,SAAS,CAAgB;IAClC;;;OAGG;IACM,OAAO,CAA2C;IAO3D,YAAY,OAQX;QACC,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QACxD,KAAK,CACH,mBAAmB,EACnB,wBAAwB,OAAO,CAAC,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,IAAI,uBAAuB,OAAO,CAAC,MAAM,GAAG,MAAM,EAAE,EACtH,OAAO,CAAC,OAAO,CAChB,CAAC;QACF,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QACzB,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;QACrC,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;QACnC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,IAAI,CAAC;QACvC,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,SAAS,EAAE;YACrC,KAAK,EAAE,OAAO,CAAC,OAAO;YACtB,UAAU,EAAE,KAAK;YACjB,QAAQ,EAAE,KAAK;SAChB,CAAC,CAAC;IACL,CAAC;CACF;AAiBD;;;GAGG;AACH,MAAM,OAAO,yBAA0B,SAAQ,iBAAiB;IAC9D,kFAAkF;IACzE,OAAO,CAA8B;IAC9C,oDAAoD;IAC3C,gBAAgB,CAAS;IAElC,YACE,KAAwB,EACxB,OAAoC,EACpC,gBAAwB;QAExB,KAAK,CAAC;YACJ,OAAO,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE;YACnD,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,OAAO,EAAE,KAAK,CAAC,OAAO;SACvB,CAAC,CAAC;QACH,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAC;QACxC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,gBAAgB,GAAG,gBAAgB,CAAC;IAC3C,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,OAAO,oBAAqB,SAAQ,oBAAoB;IACnD,IAAI,GAAG,SAAkB,CAAC;IAEnC,YAAY,OAA4B,EAAE,OAA6B;QACrE,KAAK,CACH,sBAAsB,EACtB,wBAAwB,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,IAAI,6EAA6E,EACnI,OAAO,EACP,OAAO,CACR,CAAC;IACJ,CAAC;CACF;AAED,qFAAqF;AACrF,MAAM,OAAO,oBAAqB,SAAQ,oBAAoB;IACnD,IAAI,GAAG,SAAkB,CAAC;IAC1B,SAAS,CAAS;IAE3B,YAAY,OAA4B,EAAE,SAAiB,EAAE,OAA6B;QACxF,KAAK,CACH,sBAAsB,EACtB,wBAAwB,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,IAAI,oBAAoB,SAAS,KAAK,EACxF,OAAO,EACP,OAAO,CACR,CAAC;QACF,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC7B,CAAC;CACF;AAED,gGAAgG;AAChG,MAAM,OAAO,kBAAmB,SAAQ,oBAAoB;IACjD,IAAI,GAAG,SAAkB,CAAC;IAEnC,YAAY,OAA4B,EAAE,OAA6B;QACrE,KAAK,CACH,oBAAoB,EACpB,wBAAwB,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,IAAI,4BAA4B,EAClF,OAAO,EACP,OAAO,CACR,CAAC;IACJ,CAAC;CACF;AAED,qEAAqE;AACrE,MAAM,OAAO,4BAA6B,SAAQ,oBAAoB;IAC3D,IAAI,GAAG,kBAA2B,CAAC;IACnC,MAAM,CAAS;IACxB,iEAAiE;IACxD,MAAM,CAAS;IAExB,YAAY,OAA4B,EAAE,MAAc,EAAE,MAAc;QACtE,KAAK,CACH,8BAA8B,EAC9B,wBAAwB,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,IAAI,qCAAqC,MAAM,EAAE,EACnG,OAAO,CACR,CAAC;QACF,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,CACL,KAAK,YAAY,KAAK;QACrB,KAA4C,CAAC,oBAAoB,CAAC,KAAK,IAAI,CAC7E,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Error hierarchy of the SDK.\n *\n * Every error thrown by the SDK is a {@link PixelumeError} with a stable `kind` discriminator.\n * Messages are deterministic and never include request/response bodies, headers, cookies or\n * query strings, so they are safe to log. Raw response payloads are kept on\n * {@link PixelumeHttpError.payload} as a non-enumerable property for programmatic inspection only.\n */\n\nconst PIXELUME_ERROR_BRAND = Symbol.for('pixelume.sdk.error');\n\nexport type PixelumeErrorKind =\n 'config' | 'auth' | 'http' | 'network' | 'timeout' | 'aborted' | 'invalid_response';\n\nexport type PixelumeAuthErrorReason =\n | 'browser_unavailable'\n | 'storage_unavailable'\n | 'crypto_unavailable'\n | 'invalid_callback'\n | 'state_mismatch'\n | 'authorization_expired'\n /** An operation needs a signed-in player and there is none. No request was sent. */\n | 'not_authenticated'\n /** The session ended and cannot be renewed (expired, revoked, logged out elsewhere). */\n | 'session_expired'\n /** The API rejected renewal because the game is disabled or unknown (`invalid_client`). */\n | 'game_unavailable'\n /**\n * A cross-session restoration attempt (`restoreAcrossSessions`) was refused for a reason other\n * than an ended central session, e.g. the remembered scope is no longer allowed for the game.\n */\n | 'restore_rejected'\n /**\n * Renewal is not possible: the API rejected it for a non-recoverable reason (scope, origin,\n * request) or the session has no refresh token (legacy authorization).\n */\n | 'renewal_rejected';\n\n/**\n * Error codes documented in the Pixelume API OpenAPI `ErrorEnvelope`.\n * The API may introduce new codes, so any string is accepted at runtime.\n */\nexport type PixelumeApiErrorCode =\n | 'INVALID_INPUT'\n | 'UNAUTHENTICATED'\n | 'FORBIDDEN_ORIGIN'\n | 'FORBIDDEN_HOST'\n | 'RATE_LIMITED'\n | 'REGISTRATION_DISABLED'\n | 'AUTH_TOKEN_INVALID'\n | 'OAUTH_INVALID_CLIENT'\n | 'OAUTH_INVALID_REDIRECT_URI'\n | 'OAUTH_INVALID_REQUEST'\n | 'OAUTH_INVALID_SCOPE'\n | 'OAUTH_REQUEST_EXPIRED'\n | 'OAUTH_INVALID_GRANT'\n | 'OAUTH_INVALID_TOKEN'\n | 'OAUTH_UNSUPPORTED_GRANT_TYPE'\n | 'OAUTH_INSUFFICIENT_SCOPE'\n | 'FORBIDDEN_GAME'\n | 'GAME_INACTIVE'\n | 'SAVE_NOT_FOUND'\n | 'SAVE_REVISION_CONFLICT'\n | 'SAVE_TOO_LARGE'\n | 'SAVE_QUOTA_EXCEEDED'\n | 'INTERNAL_ERROR'\n | 'NOT_FOUND'\n | (string & {});\n\n/** Identifies the request that failed. `path` never contains the query string. */\nexport interface PixelumeRequestInfo {\n readonly method: string;\n readonly path: string;\n}\n\n/** Base class of every error thrown by the SDK. */\nexport abstract class PixelumeError extends Error {\n abstract readonly kind: PixelumeErrorKind;\n\n protected constructor(name: string, message: string, options?: { cause?: unknown }) {\n super(message, options?.cause === undefined ? undefined : { cause: options.cause });\n // Explicit names survive minification of consumer bundles.\n this.name = name;\n Object.defineProperty(this, PIXELUME_ERROR_BRAND, { value: true });\n }\n}\n\n/** Invalid SDK configuration or invalid use of the SDK. Never caused by the network. */\nexport class PixelumeConfigError extends PixelumeError {\n readonly kind = 'config' as const;\n\n constructor(message: string) {\n super('PixelumeConfigError', message);\n }\n}\n\n/** A local browser authorization failure that happened before an API response was available. */\nexport class PixelumeAuthError extends PixelumeError {\n readonly kind = 'auth' as const;\n readonly reason: PixelumeAuthErrorReason;\n\n constructor(reason: PixelumeAuthErrorReason, options?: { cause?: unknown }) {\n super('PixelumeAuthError', `Pixelume authorization failed (${reason})`, options);\n this.reason = reason;\n }\n}\n\n/** Base class of errors tied to a specific HTTP request. */\nexport abstract class PixelumeRequestError extends PixelumeError {\n readonly method: string;\n readonly path: string;\n\n protected constructor(\n name: string,\n message: string,\n request: PixelumeRequestInfo,\n options?: { cause?: unknown },\n ) {\n super(name, message, options);\n this.method = request.method;\n this.path = request.path;\n }\n}\n\n/** The API answered with a non-2xx status. */\nexport class PixelumeHttpError extends PixelumeRequestError {\n readonly kind = 'http' as const;\n readonly status: number;\n /** `error.code` from the API error envelope, or `null` when the body is not an envelope. */\n readonly code: PixelumeApiErrorCode | null;\n /** `error.message` from the API error envelope (already sanitized by the API). */\n readonly apiMessage: string | null;\n /** Request ID from the envelope or the `x-request-id` header, useful for support. */\n readonly requestId: string | null;\n /**\n * `error.details` from the API envelope (e.g. `oauthError`, `quota`, conflict metadata), or\n * `null`. The API only exposes non-sensitive details here.\n */\n readonly details: Readonly<Record<string, unknown>> | null;\n /**\n * Decoded response body (JSON value, text or `undefined`). Non-enumerable so that\n * `JSON.stringify(error)` and structured loggers do not dump it by accident.\n */\n declare readonly payload: unknown;\n\n constructor(details: {\n request: PixelumeRequestInfo;\n status: number;\n code: string | null;\n apiMessage: string | null;\n requestId: string | null;\n details?: Readonly<Record<string, unknown>> | null;\n payload: unknown;\n }) {\n const suffix = details.code ? ` (${details.code})` : '';\n super(\n 'PixelumeHttpError',\n `Pixelume API request ${details.request.method} ${details.request.path} failed with status ${details.status}${suffix}`,\n details.request,\n );\n this.status = details.status;\n this.code = details.code;\n this.apiMessage = details.apiMessage;\n this.requestId = details.requestId;\n this.details = details.details ?? null;\n Object.defineProperty(this, 'payload', {\n value: details.payload,\n enumerable: false,\n writable: false,\n });\n }\n}\n\n/** Server-side metadata of a cloud save, as reported by the API. */\nexport interface PixelumeSaveMetadata {\n readonly gameId: string;\n readonly slotId: string;\n /** Version of the save format, chosen by the game (1–1 000 000). */\n readonly schemaVersion: number;\n /** Monotonic revision assigned by the server; never restarts, even after a delete. */\n readonly revision: number;\n /** UTF-8 size of the canonical JSON of `data`. */\n readonly sizeBytes: number;\n /** ISO 8601 timestamps assigned by the server. */\n readonly createdAt: string;\n readonly updatedAt: string;\n}\n\n/**\n * `409 SAVE_REVISION_CONFLICT`: the slot changed since `expectedRevision` (another device, tab or a\n * write whose response was lost). The SDK never retries or overwrites; the game decides.\n */\nexport class PixelumeSaveConflictError extends PixelumeHttpError {\n /** Current remote metadata, or `null` when the slot no longer has a live save. */\n readonly current: PixelumeSaveMetadata | null;\n /** The revision the rejected operation expected. */\n readonly expectedRevision: number;\n\n constructor(\n error: PixelumeHttpError,\n current: PixelumeSaveMetadata | null,\n expectedRevision: number,\n ) {\n super({\n request: { method: error.method, path: error.path },\n status: error.status,\n code: error.code,\n apiMessage: error.apiMessage,\n requestId: error.requestId,\n details: error.details,\n payload: error.payload,\n });\n this.name = 'PixelumeSaveConflictError';\n this.current = current;\n this.expectedRevision = expectedRevision;\n }\n}\n\n/**\n * The request did not produce an HTTP response: offline, DNS/TLS failure, connection reset,\n * or a browser CORS rejection (browsers do not distinguish these for security reasons).\n */\nexport class PixelumeNetworkError extends PixelumeRequestError {\n readonly kind = 'network' as const;\n\n constructor(request: PixelumeRequestInfo, options?: { cause?: unknown }) {\n super(\n 'PixelumeNetworkError',\n `Pixelume API request ${request.method} ${request.path} failed before a response was received (network failure or blocked by CORS)`,\n request,\n options,\n );\n }\n}\n\n/** The SDK timeout elapsed before the response (including its body) was received. */\nexport class PixelumeTimeoutError extends PixelumeRequestError {\n readonly kind = 'timeout' as const;\n readonly timeoutMs: number;\n\n constructor(request: PixelumeRequestInfo, timeoutMs: number, options?: { cause?: unknown }) {\n super(\n 'PixelumeTimeoutError',\n `Pixelume API request ${request.method} ${request.path} timed out after ${timeoutMs} ms`,\n request,\n options,\n );\n this.timeoutMs = timeoutMs;\n }\n}\n\n/** The caller aborted the request through its `AbortSignal`. `cause` holds the abort reason. */\nexport class PixelumeAbortError extends PixelumeRequestError {\n readonly kind = 'aborted' as const;\n\n constructor(request: PixelumeRequestInfo, options?: { cause?: unknown }) {\n super(\n 'PixelumeAbortError',\n `Pixelume API request ${request.method} ${request.path} was aborted by the caller`,\n request,\n options,\n );\n }\n}\n\n/** A 2xx response did not match the contract expected by the SDK. */\nexport class PixelumeInvalidResponseError extends PixelumeRequestError {\n readonly kind = 'invalid_response' as const;\n readonly status: number;\n /** Short, payload-free description of the contract violation. */\n readonly reason: string;\n\n constructor(request: PixelumeRequestInfo, status: number, reason: string) {\n super(\n 'PixelumeInvalidResponseError',\n `Pixelume API request ${request.method} ${request.path} returned an unexpected response: ${reason}`,\n request,\n );\n this.status = status;\n this.reason = reason;\n }\n}\n\n/**\n * Returns `true` for any error thrown by the SDK. Works across duplicated copies of the package\n * (where `instanceof` would fail) because it checks a global symbol brand.\n */\nexport function isPixelumeError(value: unknown): value is PixelumeError {\n return (\n value instanceof Error &&\n (value as unknown as Record<symbol, unknown>)[PIXELUME_ERROR_BRAND] === true\n );\n}\n"]}
@@ -16,6 +16,14 @@ export interface PixelumeBrowserAdapter {
16
16
  navigate(url: string): void;
17
17
  replaceUrl(url: string): void;
18
18
  readonly sessionStorage: PixelumeSessionStorage;
19
+ /**
20
+ * Optional persistent storage (normally `localStorage`) for the non-secret cross-session hint
21
+ * of `restoreAcrossSessions`. Never receives tokens. When an injected adapter omits it,
22
+ * cross-session restoration is unavailable and the SDK behaves as without the option.
23
+ */
24
+ readonly persistentStorage?: PixelumeSessionStorage | null;
25
+ /** Optional connectivity probe. Defaults to `navigator.onLine` (unknown counts as online). */
26
+ isOnline?(): boolean;
19
27
  readonly crypto: Crypto;
20
28
  /**
21
29
  * Optional: calls `onResume` when the page may have been suspended (tab visible again, restored
@@ -55,6 +63,18 @@ export interface PixelumeClientOptions {
55
63
  * a non-secret hint; access tokens remain memory-only. Defaults to `true`.
56
64
  */
57
65
  restoreSession?: boolean;
66
+ /**
67
+ * Opt-in: recover the account in a new tab or after the browser is reopened, while the central
68
+ * Pixelume session is still valid, without asking for the password again. After a successful
69
+ * login the SDK keeps a **non-secret** hint in `localStorage` (only "this game had a login on
70
+ * this browser", the granted scope and when); on a later start without an in-memory session it
71
+ * makes at most one authorization attempt with `prompt=none` — a brief top-level round trip that
72
+ * never shows a screen. If the central session ended, the API answers `login_required`, the hint
73
+ * is removed and the game continues as a visitor (`reason: 'session_ended'`), with no loop.
74
+ * Tokens stay memory-only. `true` uses a 30-day hint; `{ maxAgeDays }` (1–90) changes it.
75
+ * Defaults to `false`.
76
+ */
77
+ restoreAcrossSessions?: boolean | PixelumeCrossSessionRestoreOptions;
58
78
  /** Per-request timeout in milliseconds. Defaults to 10 000. */
59
79
  timeoutMs?: number;
60
80
  /**
@@ -70,6 +90,13 @@ export interface PixelumeClientOptions {
70
90
  /** Advanced/test-only browser capability injection. Defaults to the current browser globals. */
71
91
  browser?: PixelumeBrowserAdapter;
72
92
  }
93
+ export interface PixelumeCrossSessionRestoreOptions {
94
+ /**
95
+ * Days after the last successful login during which a new tab/browser start tries to recover the
96
+ * account. Defaults to 30 (the central session lifetime). Integer from 1 to 90.
97
+ */
98
+ readonly maxAgeDays?: number;
99
+ }
73
100
  /** Effective, validated configuration of a client. */
74
101
  export interface PixelumeClientConfig {
75
102
  readonly apiUrl: string;
@@ -79,6 +106,8 @@ export interface PixelumeClientConfig {
79
106
  readonly scope: 'profile' | 'profile saves';
80
107
  readonly authUrl: string;
81
108
  readonly restoreSession: boolean;
109
+ /** Whether cross-session restoration (`restoreAcrossSessions`) is enabled. */
110
+ readonly restoreAcrossSessions: boolean;
82
111
  readonly timeoutMs: number;
83
112
  readonly credentials: PixelumeCredentialsPolicy;
84
113
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { CredentialsPolicy, FetchLike } from '../http/types.js';\n\nexport type PixelumeCredentialsPolicy = CredentialsPolicy;\nexport type PixelumeFetch = FetchLike;\n\n/** Minimal browser storage contract used for transient PKCE state and a non-secret session hint. */\nexport interface PixelumeSessionStorage {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\n/**\n * Browser capabilities used by the redirect flow. Usually omitted; injection is useful in tests,\n * embedded runtimes and non-standard webviews.\n */\nexport interface PixelumeBrowserAdapter {\n currentUrl(): string;\n navigate(url: string): void;\n replaceUrl(url: string): void;\n readonly sessionStorage: PixelumeSessionStorage;\n readonly crypto: Crypto;\n /**\n * Optional: calls `onResume` when the page may have been suspended (tab visible again, restored\n * from the back/forward cache, back online) and returns an unsubscribe function. When omitted,\n * the SDK listens to `visibilitychange`, `pageshow` and `online` on the browser globals.\n */\n subscribeResume?(onResume: () => void): () => void;\n}\n\nexport interface PixelumeClientOptions {\n /**\n * Base URL of the Pixelume API. Defaults to `https://api.pixelume.com.br`.\n * Must be `https:`; `http:` is accepted for loopback hosts during local development.\n */\n apiUrl?: string;\n /**\n * Public identifier of the game, e.g. `rail-puzzle`. Optional in Phase 1 because no current\n * API contract uses it; future modules that need it will fail fast with a config error.\n * It is an identifier, never an authorization credential.\n */\n gameId?: string;\n /**\n * OAuth public client identifier registered by the Pixelume API. Defaults to `gameId`.\n * This is public metadata, never a secret.\n */\n clientId?: string;\n /** Exact callback URI registered for the OAuth client. Defaults to `<origin>/oauth/callback`. */\n redirectUri?: string;\n /**\n * Scope requested by `auth.login()`. Use `'profile saves'` for Cloud Saves; the game must have\n * `saves` enabled in the Pixelume Game Registry. Defaults to `'profile'`.\n */\n scope?: 'profile' | 'profile saves';\n /** Pixelume Auth portal origin. Defaults to `https://auth.pixelume.com.br`. */\n authUrl?: string;\n /**\n * Reauthorize after a same-tab reload when a previous SDK session existed. The SDK stores only\n * a non-secret hint; access tokens remain memory-only. Defaults to `true`.\n */\n restoreSession?: boolean;\n /** Per-request timeout in milliseconds. Defaults to 10 000. */\n timeoutMs?: number;\n /**\n * `fetch` credentials policy used by modules that do not set their own. Defaults to `'omit'`.\n * Cross-origin cookies are subject to the API's CORS allowlist and to browser policies;\n * the SDK never assumes they work.\n */\n credentials?: PixelumeCredentialsPolicy;\n /** Custom `fetch` implementation (tests, SSR, instrumentation). Defaults to `globalThis.fetch`. */\n fetch?: PixelumeFetch;\n /** Allows `http:` for non-loopback hosts (e.g. a LAN IP). Never enable it in production. */\n allowInsecureHttp?: boolean;\n /** Advanced/test-only browser capability injection. Defaults to the current browser globals. */\n browser?: PixelumeBrowserAdapter;\n}\n\n/** Effective, validated configuration of a client. */\nexport interface PixelumeClientConfig {\n readonly apiUrl: string;\n readonly gameId: string | null;\n readonly clientId: string | null;\n readonly redirectUri: string | null;\n readonly scope: 'profile' | 'profile saves';\n readonly authUrl: string;\n readonly restoreSession: boolean;\n readonly timeoutMs: number;\n readonly credentials: PixelumeCredentialsPolicy;\n}\n\n/** Options accepted by every SDK operation. */\nexport interface PixelumeRequestOptions {\n /** Aborts the operation; the promise rejects with `PixelumeAbortError`. */\n signal?: AbortSignal;\n /** Overrides the client timeout for this operation. */\n timeoutMs?: number;\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { CredentialsPolicy, FetchLike } from '../http/types.js';\n\nexport type PixelumeCredentialsPolicy = CredentialsPolicy;\nexport type PixelumeFetch = FetchLike;\n\n/** Minimal browser storage contract used for transient PKCE state and a non-secret session hint. */\nexport interface PixelumeSessionStorage {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\n/**\n * Browser capabilities used by the redirect flow. Usually omitted; injection is useful in tests,\n * embedded runtimes and non-standard webviews.\n */\nexport interface PixelumeBrowserAdapter {\n currentUrl(): string;\n navigate(url: string): void;\n replaceUrl(url: string): void;\n readonly sessionStorage: PixelumeSessionStorage;\n /**\n * Optional persistent storage (normally `localStorage`) for the non-secret cross-session hint\n * of `restoreAcrossSessions`. Never receives tokens. When an injected adapter omits it,\n * cross-session restoration is unavailable and the SDK behaves as without the option.\n */\n readonly persistentStorage?: PixelumeSessionStorage | null;\n /** Optional connectivity probe. Defaults to `navigator.onLine` (unknown counts as online). */\n isOnline?(): boolean;\n readonly crypto: Crypto;\n /**\n * Optional: calls `onResume` when the page may have been suspended (tab visible again, restored\n * from the back/forward cache, back online) and returns an unsubscribe function. When omitted,\n * the SDK listens to `visibilitychange`, `pageshow` and `online` on the browser globals.\n */\n subscribeResume?(onResume: () => void): () => void;\n}\n\nexport interface PixelumeClientOptions {\n /**\n * Base URL of the Pixelume API. Defaults to `https://api.pixelume.com.br`.\n * Must be `https:`; `http:` is accepted for loopback hosts during local development.\n */\n apiUrl?: string;\n /**\n * Public identifier of the game, e.g. `rail-puzzle`. Optional in Phase 1 because no current\n * API contract uses it; future modules that need it will fail fast with a config error.\n * It is an identifier, never an authorization credential.\n */\n gameId?: string;\n /**\n * OAuth public client identifier registered by the Pixelume API. Defaults to `gameId`.\n * This is public metadata, never a secret.\n */\n clientId?: string;\n /** Exact callback URI registered for the OAuth client. Defaults to `<origin>/oauth/callback`. */\n redirectUri?: string;\n /**\n * Scope requested by `auth.login()`. Use `'profile saves'` for Cloud Saves; the game must have\n * `saves` enabled in the Pixelume Game Registry. Defaults to `'profile'`.\n */\n scope?: 'profile' | 'profile saves';\n /** Pixelume Auth portal origin. Defaults to `https://auth.pixelume.com.br`. */\n authUrl?: string;\n /**\n * Reauthorize after a same-tab reload when a previous SDK session existed. The SDK stores only\n * a non-secret hint; access tokens remain memory-only. Defaults to `true`.\n */\n restoreSession?: boolean;\n /**\n * Opt-in: recover the account in a new tab or after the browser is reopened, while the central\n * Pixelume session is still valid, without asking for the password again. After a successful\n * login the SDK keeps a **non-secret** hint in `localStorage` (only \"this game had a login on\n * this browser\", the granted scope and when); on a later start without an in-memory session it\n * makes at most one authorization attempt with `prompt=none` — a brief top-level round trip that\n * never shows a screen. If the central session ended, the API answers `login_required`, the hint\n * is removed and the game continues as a visitor (`reason: 'session_ended'`), with no loop.\n * Tokens stay memory-only. `true` uses a 30-day hint; `{ maxAgeDays }` (1–90) changes it.\n * Defaults to `false`.\n */\n restoreAcrossSessions?: boolean | PixelumeCrossSessionRestoreOptions;\n /** Per-request timeout in milliseconds. Defaults to 10 000. */\n timeoutMs?: number;\n /**\n * `fetch` credentials policy used by modules that do not set their own. Defaults to `'omit'`.\n * Cross-origin cookies are subject to the API's CORS allowlist and to browser policies;\n * the SDK never assumes they work.\n */\n credentials?: PixelumeCredentialsPolicy;\n /** Custom `fetch` implementation (tests, SSR, instrumentation). Defaults to `globalThis.fetch`. */\n fetch?: PixelumeFetch;\n /** Allows `http:` for non-loopback hosts (e.g. a LAN IP). Never enable it in production. */\n allowInsecureHttp?: boolean;\n /** Advanced/test-only browser capability injection. Defaults to the current browser globals. */\n browser?: PixelumeBrowserAdapter;\n}\n\nexport interface PixelumeCrossSessionRestoreOptions {\n /**\n * Days after the last successful login during which a new tab/browser start tries to recover the\n * account. Defaults to 30 (the central session lifetime). Integer from 1 to 90.\n */\n readonly maxAgeDays?: number;\n}\n\n/** Effective, validated configuration of a client. */\nexport interface PixelumeClientConfig {\n readonly apiUrl: string;\n readonly gameId: string | null;\n readonly clientId: string | null;\n readonly redirectUri: string | null;\n readonly scope: 'profile' | 'profile saves';\n readonly authUrl: string;\n readonly restoreSession: boolean;\n /** Whether cross-session restoration (`restoreAcrossSessions`) is enabled. */\n readonly restoreAcrossSessions: boolean;\n readonly timeoutMs: number;\n readonly credentials: PixelumeCredentialsPolicy;\n}\n\n/** Options accepted by every SDK operation. */\nexport interface PixelumeRequestOptions {\n /** Aborts the operation; the promise rejects with `PixelumeAbortError`. */\n signal?: AbortSignal;\n /** Overrides the client timeout for this operation. */\n timeoutMs?: number;\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export { DEFAULT_API_URL, DEFAULT_AUTH_URL, DEFAULT_TIMEOUT_MS } from './core/co
3
3
  export { SDK_VERSION } from './version.js';
4
4
  export { PixelumeError, PixelumeConfigError, PixelumeAuthError, PixelumeRequestError, PixelumeHttpError, PixelumeNetworkError, PixelumeTimeoutError, PixelumeAbortError, PixelumeInvalidResponseError, PixelumeSaveConflictError, isPixelumeError, } from './core/errors.js';
5
5
  export type { PixelumeErrorKind, PixelumeApiErrorCode, PixelumeAuthErrorReason, PixelumeRequestInfo, PixelumeSaveMetadata, } from './core/errors.js';
6
- export type { PixelumeClientOptions, PixelumeClientConfig, PixelumeRequestOptions, PixelumeCredentialsPolicy, PixelumeFetch, PixelumeBrowserAdapter, PixelumeSessionStorage, } from './core/types.js';
6
+ export type { PixelumeClientOptions, PixelumeClientConfig, PixelumeRequestOptions, PixelumeCredentialsPolicy, PixelumeFetch, PixelumeBrowserAdapter, PixelumeSessionStorage, PixelumeCrossSessionRestoreOptions, } from './core/types.js';
7
7
  export type { PixelumeHealthApi, PixelumeHealthStatus, PixelumeRegistrationMode, } from './modules/health/HealthModule.js';
8
8
  export type { PixelumeAuthApi, PixelumeAuthState, PixelumeAuthListener, PixelumeAnonymousReason, PixelumeLoginOptions, PixelumePlayer, PixelumeScopeRequest, } from './modules/auth/AuthModule.js';
9
9
  export type { PixelumeScope, PixelumeSessionState, PixelumeSessionListener, } from './modules/auth/SessionManager.js';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACzF,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EACL,aAAa,EACb,mBAAmB,EACnB,iBAAiB,EACjB,oBAAoB,EACpB,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,kBAAkB,EAClB,4BAA4B,EAC5B,yBAAyB,EACzB,eAAe,GAChB,MAAM,kBAAkB,CAAC","sourcesContent":["export { PixelumeClient, Pixelume } from './core/PixelumeClient.js';\nexport { DEFAULT_API_URL, DEFAULT_AUTH_URL, DEFAULT_TIMEOUT_MS } from './core/config.js';\nexport { SDK_VERSION } from './version.js';\nexport {\n PixelumeError,\n PixelumeConfigError,\n PixelumeAuthError,\n PixelumeRequestError,\n PixelumeHttpError,\n PixelumeNetworkError,\n PixelumeTimeoutError,\n PixelumeAbortError,\n PixelumeInvalidResponseError,\n PixelumeSaveConflictError,\n isPixelumeError,\n} from './core/errors.js';\nexport type {\n PixelumeErrorKind,\n PixelumeApiErrorCode,\n PixelumeAuthErrorReason,\n PixelumeRequestInfo,\n PixelumeSaveMetadata,\n} from './core/errors.js';\nexport type {\n PixelumeClientOptions,\n PixelumeClientConfig,\n PixelumeRequestOptions,\n PixelumeCredentialsPolicy,\n PixelumeFetch,\n PixelumeBrowserAdapter,\n PixelumeSessionStorage,\n} from './core/types.js';\nexport type {\n PixelumeHealthApi,\n PixelumeHealthStatus,\n PixelumeRegistrationMode,\n} from './modules/health/HealthModule.js';\nexport type {\n PixelumeAuthApi,\n PixelumeAuthState,\n PixelumeAuthListener,\n PixelumeAnonymousReason,\n PixelumeLoginOptions,\n PixelumePlayer,\n PixelumeScopeRequest,\n} from './modules/auth/AuthModule.js';\nexport type {\n PixelumeScope,\n PixelumeSessionState,\n PixelumeSessionListener,\n} from './modules/auth/SessionManager.js';\nexport type {\n PixelumeSavesApi,\n PixelumeSave,\n PixelumeSaveList,\n PixelumeSaveWrite,\n PixelumeSaveResult,\n PixelumeSaveDeleteOptions,\n PixelumeSaveDeleteResult,\n PixelumeSavesActivity,\n PixelumeSavesListener,\n PixelumeSavesOperation,\n} from './modules/saves/SavesModule.js';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACzF,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EACL,aAAa,EACb,mBAAmB,EACnB,iBAAiB,EACjB,oBAAoB,EACpB,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,kBAAkB,EAClB,4BAA4B,EAC5B,yBAAyB,EACzB,eAAe,GAChB,MAAM,kBAAkB,CAAC","sourcesContent":["export { PixelumeClient, Pixelume } from './core/PixelumeClient.js';\nexport { DEFAULT_API_URL, DEFAULT_AUTH_URL, DEFAULT_TIMEOUT_MS } from './core/config.js';\nexport { SDK_VERSION } from './version.js';\nexport {\n PixelumeError,\n PixelumeConfigError,\n PixelumeAuthError,\n PixelumeRequestError,\n PixelumeHttpError,\n PixelumeNetworkError,\n PixelumeTimeoutError,\n PixelumeAbortError,\n PixelumeInvalidResponseError,\n PixelumeSaveConflictError,\n isPixelumeError,\n} from './core/errors.js';\nexport type {\n PixelumeErrorKind,\n PixelumeApiErrorCode,\n PixelumeAuthErrorReason,\n PixelumeRequestInfo,\n PixelumeSaveMetadata,\n} from './core/errors.js';\nexport type {\n PixelumeClientOptions,\n PixelumeClientConfig,\n PixelumeRequestOptions,\n PixelumeCredentialsPolicy,\n PixelumeFetch,\n PixelumeBrowserAdapter,\n PixelumeSessionStorage,\n PixelumeCrossSessionRestoreOptions,\n} from './core/types.js';\nexport type {\n PixelumeHealthApi,\n PixelumeHealthStatus,\n PixelumeRegistrationMode,\n} from './modules/health/HealthModule.js';\nexport type {\n PixelumeAuthApi,\n PixelumeAuthState,\n PixelumeAuthListener,\n PixelumeAnonymousReason,\n PixelumeLoginOptions,\n PixelumePlayer,\n PixelumeScopeRequest,\n} from './modules/auth/AuthModule.js';\nexport type {\n PixelumeScope,\n PixelumeSessionState,\n PixelumeSessionListener,\n} from './modules/auth/SessionManager.js';\nexport type {\n PixelumeSavesApi,\n PixelumeSave,\n PixelumeSaveList,\n PixelumeSaveWrite,\n PixelumeSaveResult,\n PixelumeSaveDeleteOptions,\n PixelumeSaveDeleteResult,\n PixelumeSavesActivity,\n PixelumeSavesListener,\n PixelumeSavesOperation,\n} from './modules/saves/SavesModule.js';\n"]}
@@ -8,16 +8,35 @@ export interface PixelumePlayer {
8
8
  readonly displayName: string;
9
9
  readonly avatarUrl: string | null;
10
10
  }
11
- export type PixelumeAnonymousReason = 'visitor' | 'cancelled' | 'expired' | 'logged_out' | 'error';
11
+ /**
12
+ * Why there is no signed-in player:
13
+ * - `visitor`: no login to restore in this tab/browser (or restoration is off). Not proof that the
14
+ * central session ended.
15
+ * - `cancelled`: the player cancelled the login in the portal.
16
+ * - `expired`: a session that was running ended (renewal refused).
17
+ * - `logged_out`: `auth.logout()`.
18
+ * - `session_ended`: a cross-session restoration attempt found no valid central session
19
+ * (`login_required`); the hint was removed and no further automatic attempt is made.
20
+ * - `offline`: a cross-session restoration could not run or finish for lack of connectivity. The
21
+ * hint is kept; the next start (or `login()`) tries again. Local progress is untouched.
22
+ * - `error`: anything else; see `error`.
23
+ */
24
+ export type PixelumeAnonymousReason = 'visitor' | 'cancelled' | 'expired' | 'logged_out' | 'session_ended' | 'offline' | 'error';
25
+ /**
26
+ * `restoring: true` marks the silent cross-session attempt (`restoreAcrossSessions`): the page is
27
+ * leaving for a brief round trip without UI, or completing it. Interactive logins omit it.
28
+ */
12
29
  export type PixelumeAuthState = {
13
30
  readonly status: 'uninitialized';
14
31
  readonly player: null;
15
32
  } | {
16
33
  readonly status: 'redirecting';
17
34
  readonly player: null;
35
+ readonly restoring?: true;
18
36
  } | {
19
37
  readonly status: 'exchanging';
20
38
  readonly player: null;
39
+ readonly restoring?: true;
21
40
  } | {
22
41
  readonly status: 'anonymous';
23
42
  readonly player: null;