@pixelume/sdk 0.4.0 → 0.6.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 +62 -1
- package/README.md +97 -19
- package/dist/core/PixelumeClient.d.ts +10 -2
- package/dist/core/PixelumeClient.js +20 -2
- package/dist/core/PixelumeClient.js.map +1 -1
- package/dist/core/config.d.ts +18 -1
- package/dist/core/config.js +99 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/errors.d.ts +5 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/types.d.ts +78 -0
- package/dist/core/types.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/analytics/AnalyticsModule.d.ts +93 -0
- package/dist/modules/analytics/AnalyticsModule.js +461 -0
- package/dist/modules/analytics/AnalyticsModule.js.map +1 -0
- package/dist/modules/analytics/AnalyticsProvider.d.ts +51 -0
- package/dist/modules/analytics/AnalyticsProvider.js +23 -0
- package/dist/modules/analytics/AnalyticsProvider.js.map +1 -0
- package/dist/modules/analytics/PostHogAnalyticsProvider.d.ts +39 -0
- package/dist/modules/analytics/PostHogAnalyticsProvider.js +255 -0
- package/dist/modules/analytics/PostHogAnalyticsProvider.js.map +1 -0
- package/dist/modules/analytics/events.d.ts +30 -0
- package/dist/modules/analytics/events.js +153 -0
- package/dist/modules/analytics/events.js.map +1 -0
- package/dist/modules/auth/AuthModule.d.ts +20 -1
- package/dist/modules/auth/AuthModule.js +305 -21
- package/dist/modules/auth/AuthModule.js.map +1 -1
- package/dist/ui/widget.js +15 -5
- 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 +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,65 @@ conter mudanças incompatíveis, sempre listadas em **Changed/Removed**.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.0] - 2026-10-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `pixelume.analytics` (opt-in): `track`, `startSession`/`endSession` (sessão de **jogo**, idempotentes),
|
|
15
|
+
`levelStarted`/`levelCompleted`/`levelFailed`, `setConsent`, `isEnabled` e os estados `consent`,
|
|
16
|
+
`status` e `sessionId`. Catálogo estável: `game_session_started`, `game_session_ended` (com
|
|
17
|
+
`duration_ms` e `visible_duration_ms`), `level_started`, `level_completed`, `level_failed`. Todo
|
|
18
|
+
evento leva `game_id`, `game_version`, `sdk_version`, `session_id` e `platform: 'web'`, protegidos
|
|
19
|
+
contra sobrescrita; nomes e propriedades são validados e limitados.
|
|
20
|
+
- Opção `analytics: { posthog: { projectToken, host }, gameVersion, rememberConsent, onIssue }`, campo
|
|
21
|
+
`config.analytics` e tipos `PixelumeAnalyticsApi`, `PixelumeAnalyticsOptions`, `PixelumePostHogOptions`,
|
|
22
|
+
`PixelumeAnalyticsConsent`, `PixelumeAnalyticsStatus`, `PixelumeAnalyticsProperties`,
|
|
23
|
+
`PixelumeAnalyticsIssue`. Token, host ou `gameVersion` vazios (variável de build definida sem valor)
|
|
24
|
+
equivalem a ausentes: analytics fica no-op, ou o evento sai sem `game_version`.
|
|
25
|
+
- Provider PostHog: `posthog-js` (`1.435.8`, exata) carregado por `import()` dinâmico **somente após o
|
|
26
|
+
consentimento**, em chunk separado (~52 KB gzip); instância própria com storage prefixado; autocapture,
|
|
27
|
+
pageviews, replay, exceções, surveys, flags/remote config e scripts externos desligados; URLs sem query
|
|
28
|
+
string. Revogar descarta o que estava pendente (inclusive o lote interno do PostHog), reseta a
|
|
29
|
+
identidade e apaga somente os identificadores do próprio jogo (outro jogo na mesma origem, mesmo com
|
|
30
|
+
`gameId` de mesmo prefixo, não é afetado). Login identifica com `player.id`; logout/expiração separam.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- O pacote passa a ter uma dependência de runtime (`posthog-js`), baixada pelo navegador apenas com
|
|
35
|
+
analytics configurado e consentido. Sem a opção `analytics`, o comportamento é idêntico ao da 0.5.0
|
|
36
|
+
(no-op: sem rede, storage ou listeners); o chunk principal cresce ~5 KB gzip.
|
|
37
|
+
|
|
38
|
+
## [0.5.0] - 2026-10-14
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- `restoreAcrossSessions` (opt-in, padrão `false`): recupera a conta numa **aba nova ou após reabrir o
|
|
43
|
+
navegador** enquanto a sessão central da Pixelume for válida, sem pedir senha. Após um login, o SDK
|
|
44
|
+
guarda em `localStorage` só um indício não secreto (`pixelume.oauth.restore.<clientId>`: versão,
|
|
45
|
+
escopo concedido e data) e, num início sem sessão em memória, faz **no máximo uma** autorização
|
|
46
|
+
`prompt=none` (ida e volta de topo, sem tela). Requer a Pixelume API com `prompt=none`
|
|
47
|
+
(implantada em 14/10/2026). `true` usa 30 dias; `{ maxAgeDays }` aceita de 1 a 90.
|
|
48
|
+
- Estado `redirecting`/`exchanging` com `restoring: true` durante a tentativa silenciosa.
|
|
49
|
+
- Motivos anônimos `session_ended` (a API respondeu `login_required`/`consent_required`/
|
|
50
|
+
`interaction_required`: o indício é removido e nada é tentado de novo automaticamente) e `offline`
|
|
51
|
+
(sem conexão para tentar ou concluir a recuperação: o indício é mantido).
|
|
52
|
+
- `PixelumeAuthError` com `reason: 'restore_rejected'` quando a recuperação é recusada por outro
|
|
53
|
+
motivo (por exemplo, `saves` deixou de ser permitido: o indício passa a pedir só `profile`).
|
|
54
|
+
- `PixelumeBrowserAdapter.persistentStorage` e `isOnline()` opcionais; tipo
|
|
55
|
+
`PixelumeCrossSessionRestoreOptions`; `config.restoreAcrossSessions`.
|
|
56
|
+
- Widget: “Conectando à sua conta Pixelume…” durante a recuperação e textos para sessão encerrada e
|
|
57
|
+
falta de conexão.
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
|
|
61
|
+
- Com `restoreAcrossSessions`, o reload da mesma aba também usa `prompt=none` em vez de passar pelo
|
|
62
|
+
portal: se a sessão central tiver acabado, o jogador continua como visitante em vez de ver a tela de
|
|
63
|
+
login. Sem a opção, o comportamento é idêntico ao da 0.4.0 (um indício deixado por uma versão com a
|
|
64
|
+
opção ligada é apenas removido).
|
|
65
|
+
- Proteções contra loop: uma tentativa por início, marcador por aba que impede nova tentativa por
|
|
66
|
+
10 minutos se a anterior não voltar, callback repetido recusado sem troca de código, retorno do
|
|
67
|
+
back/forward cache durante a tentativa vira visitante. Tokens continuam só em memória.
|
|
68
|
+
|
|
10
69
|
## [0.4.0] - 2026-10-14
|
|
11
70
|
|
|
12
71
|
### Added
|
|
@@ -106,7 +165,9 @@ conter mudanças incompatíveis, sempre listadas em **Changed/Removed**.
|
|
|
106
165
|
`invalid_response`) e `isPixelumeError()`.
|
|
107
166
|
- Build ESM + `.d.ts`, CI e workflow de publicação via npm Trusted Publishing.
|
|
108
167
|
|
|
109
|
-
[Unreleased]: https://github.com/ericksonnascimento/pixelume-sdk/compare/v0.
|
|
168
|
+
[Unreleased]: https://github.com/ericksonnascimento/pixelume-sdk/compare/v0.6.0...HEAD
|
|
169
|
+
[0.6.0]: https://github.com/ericksonnascimento/pixelume-sdk/compare/v0.5.0...v0.6.0
|
|
170
|
+
[0.5.0]: https://github.com/ericksonnascimento/pixelume-sdk/compare/v0.4.0...v0.5.0
|
|
110
171
|
[0.4.0]: https://github.com/ericksonnascimento/pixelume-sdk/compare/v0.3.0...v0.4.0
|
|
111
172
|
[0.3.0]: https://github.com/ericksonnascimento/pixelume-sdk/compare/v0.2.0...v0.3.0
|
|
112
173
|
[0.2.0]: https://github.com/ericksonnascimento/pixelume-sdk/compare/v0.1.1...v0.2.0
|
package/README.md
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
SDK TypeScript para jogos web da Pixelume. O core headless continua sendo o padrão e há um widget
|
|
4
4
|
oficial opt-in em `@pixelume/sdk/ui`. Os dois modos usam a mesma autenticação centralizada com OAuth 2.0
|
|
5
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`).
|
|
7
|
-
no jogo nem
|
|
6
|
+
rotativos, só em memória) e **Cloud Saves** (`pixelume.saves`). A versão 0.6 acrescenta **analytics
|
|
7
|
+
de gameplay opt-in** (`pixelume.analytics`), desacoplado do provider. Não há senha no jogo nem
|
|
8
|
+
`client_secret`; a única dependência de runtime (`posthog-js`) só é baixada pelo navegador depois do
|
|
9
|
+
consentimento do jogador, num chunk separado.
|
|
8
10
|
|
|
9
11
|
## Instalação
|
|
10
12
|
|
|
@@ -103,6 +105,34 @@ nunca sobrescreve, mescla ou reenvia sozinho, e não há fila offline. Saves sã
|
|
|
103
105
|
verificados: não servem como prova de conquista, ranking ou prêmio. Guia completo:
|
|
104
106
|
[Cloud Saves](docs/CLOUD_SAVES.md).
|
|
105
107
|
|
|
108
|
+
## Analytics (opt-in, com consentimento)
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const pixelume = new Pixelume({
|
|
112
|
+
gameId: 'rail-puzzle',
|
|
113
|
+
analytics: {
|
|
114
|
+
posthog: { projectToken: '<POSTHOG_PROJECT_TOKEN>', host: '<POSTHOG_INGESTION_HOST>' },
|
|
115
|
+
gameVersion: '1.0.0',
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (pixelume.analytics.consent === 'unknown') showConsentPrompt();
|
|
120
|
+
consentButton.onclick = () => pixelume.analytics.setConsent(true); // false revoga e apaga ids
|
|
121
|
+
|
|
122
|
+
pixelume.analytics.startSession(); // sessão de JOGO, não de login
|
|
123
|
+
pixelume.analytics.levelStarted(3);
|
|
124
|
+
pixelume.analytics.levelCompleted(3, { stars: 2 });
|
|
125
|
+
pixelume.analytics.levelFailed(4, 'out_of_moves');
|
|
126
|
+
pixelume.analytics.track('hint_used', { source: 'menu' });
|
|
127
|
+
pixelume.analytics.endSession();
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Sem a opção `analytics` (ou sem token/host), tudo é no-op: nenhuma rede, storage ou download. Com ela,
|
|
131
|
+
nada é enviado, gravado nem baixado antes de `setConsent(true)`; autocapture, pageviews, replay, surveys,
|
|
132
|
+
flags e scripts externos ficam desligados. Os jogos nunca importam `posthog-js` e nenhum método lança
|
|
133
|
+
exceção nem bloqueia o jogo. Catálogo de eventos, identidade, privacidade, CSP e impacto no bundle:
|
|
134
|
+
[guia de analytics](docs/ANALYTICS.md).
|
|
135
|
+
|
|
106
136
|
Importar ou instanciar o core não cria elementos, não injeta estilos e não ativa UI. O health check
|
|
107
137
|
também é totalmente independente: chamar `/health` nunca monta o widget.
|
|
108
138
|
|
|
@@ -141,8 +171,11 @@ avisos de conexão durante a renovação e, apenas depois de `saves.save()`/`sav
|
|
|
141
171
|
status da nuvem.
|
|
142
172
|
|
|
143
173
|
`pixelume.auth.state.status` é `uninitialized`, `redirecting`, `exchanging`, `anonymous` ou
|
|
144
|
-
`authenticated`.
|
|
145
|
-
|
|
174
|
+
`authenticated`. `redirecting`/`exchanging` trazem `restoring: true` durante a recuperação silenciosa
|
|
175
|
+
entre sessões. O estado anônimo informa `reason` (`visitor`, `cancelled`, `expired`, `logged_out`,
|
|
176
|
+
`session_ended`, `offline` ou `error`) e, em falha, um `PixelumeError`. Cancelar no portal resulta em
|
|
177
|
+
visitante, não em exceção. `visitor` nunca significa “a sessão central acabou”: só que não havia o que
|
|
178
|
+
recuperar nesta aba/navegador.
|
|
146
179
|
|
|
147
180
|
### Visitante, recarga e expiração
|
|
148
181
|
|
|
@@ -153,6 +186,7 @@ status da nuvem.
|
|
|
153
186
|
tinha uma sessão (e o escopo concedido). Em uma recarga, inicia uma nova autorização com o mesmo
|
|
154
187
|
escopo; enquanto a sessão central da API continuar válida, o portal retorna sem pedir senha.
|
|
155
188
|
`restoreSession: false` desliga esse comportamento.
|
|
189
|
+
- Aba nova ou navegador reaberto: veja [Recuperação entre sessões](#recuperação-entre-sessões-do-navegador-opt-in).
|
|
156
190
|
- PKCE `state`/`code_verifier` ficam na sessão por no máximo 10 minutos e são removidos antes da troca.
|
|
157
191
|
- Código expirado/repetido, `state` divergente, callback adulterado e 401 deixam o jogo utilizável como
|
|
158
192
|
visitante. O jogo pode oferecer o login novamente.
|
|
@@ -160,21 +194,60 @@ status da nuvem.
|
|
|
160
194
|
`/v1/oauth/revoke` (o que encerra todos os tokens daquela autorização). A sessão central do portal
|
|
161
195
|
continua existente por desenho; entrar novamente pode ser imediato.
|
|
162
196
|
|
|
197
|
+
### Recuperação entre sessões do navegador (opt-in)
|
|
198
|
+
|
|
199
|
+
Sem a opção, uma aba nova ou o navegador reaberto começam como visitante, mesmo com a sessão central
|
|
200
|
+
da Pixelume ainda válida (30 dias). Com `restoreAcrossSessions: true`:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const pixelume = new Pixelume({
|
|
204
|
+
gameId: 'meu-jogo',
|
|
205
|
+
scope: 'profile saves',
|
|
206
|
+
restoreAcrossSessions: true, // ou { maxAgeDays: 7 }
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
1. Depois de um login, o SDK grava em `localStorage` somente `pixelume.oauth.restore.<clientId>` =
|
|
211
|
+
`{ version, scope, at }` — nenhum token, código, verifier, ID ou e-mail.
|
|
212
|
+
2. Num início sem sessão em memória (aba nova, navegador reaberto ou reload), se esse indício existir
|
|
213
|
+
e tiver menos de `maxAgeDays` (padrão 30), `initialize()` publica
|
|
214
|
+
`{ status: 'redirecting', restoring: true }` e faz uma navegação de topo para
|
|
215
|
+
`/v1/oauth/authorize?…&prompt=none`. A API **não mostra tela**: devolve o `code` (PKCE S256 + `state`)
|
|
216
|
+
se a sessão central for válida, ou `error=login_required`.
|
|
217
|
+
3. No retorno: sucesso → `authenticated` (o escopo pedido nunca passa do anterior nem do configurado), e o
|
|
218
|
+
SDK devolve a URL original (caminho, query e hash) com `history.replaceState`. `login_required` →
|
|
219
|
+
`anonymous/session_ended`, o indício é apagado e **nenhuma** tentativa automática se repete até um novo
|
|
220
|
+
login manual.
|
|
221
|
+
4. Sem conexão no início → `anonymous/offline`, sem navegação, indício mantido (o próximo início tenta).
|
|
222
|
+
|
|
223
|
+
**UX:** a tentativa é uma ida e volta de topo de poucas centenas de milissegundos; o jogo carrega duas
|
|
224
|
+
vezes. Mostre “Conectando…” enquanto `status` for `uninitialized`/`redirecting`/`exchanging` e, se o jogo
|
|
225
|
+
roteia pelo endereço, leia a rota depois de `initialize()` (ou guarde-a ao ver `redirecting`). Estado em
|
|
226
|
+
memória do jogo antes de `initialize()` se perde na navegação.
|
|
227
|
+
|
|
228
|
+
**Proteções:** uma tentativa por início; um marcador por aba (`sessionStorage`) impede nova tentativa por
|
|
229
|
+
10 minutos se a anterior não voltar (erro de rede, botão Voltar, back/forward cache → `visitor`); callback
|
|
230
|
+
repetido é recusado sem troca de código; cada aba usa seu próprio `state`/verifier; `logout()` em qualquer
|
|
231
|
+
aba apaga o indício. Sem `localStorage` disponível (bloqueado, adaptador injetado sem
|
|
232
|
+
`persistentStorage`), o comportamento é o da 0.4. Com a opção desligada, um indício antigo é removido.
|
|
233
|
+
|
|
163
234
|
## Configuração
|
|
164
235
|
|
|
165
|
-
| Opção
|
|
166
|
-
|
|
|
167
|
-
| `apiUrl`
|
|
168
|
-
| `authUrl`
|
|
169
|
-
| `gameId`
|
|
170
|
-
| `clientId`
|
|
171
|
-
| `redirectUri`
|
|
172
|
-
| `scope`
|
|
173
|
-
| `restoreSession`
|
|
174
|
-
| `
|
|
175
|
-
| `
|
|
176
|
-
| `
|
|
177
|
-
| `
|
|
236
|
+
| Opção | Padrão | Observação |
|
|
237
|
+
| ----------------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
|
|
238
|
+
| `apiUrl` | `https://api.pixelume.com.br` | HTTPS; HTTP só em loopback ou com `allowInsecureHttp` explícito |
|
|
239
|
+
| `authUrl` | `https://auth.pixelume.com.br` | Portal esperado; a API continua sendo a autoridade que escolhe o redirect |
|
|
240
|
+
| `gameId` | — | slug público do jogo; também é o `clientId` padrão |
|
|
241
|
+
| `clientId` | `gameId` | identificador OAuth público cadastrado; nunca é credencial |
|
|
242
|
+
| `redirectUri` | `<origin>/oauth/callback` no browser | deve coincidir exatamente com o cadastro e não pode conter fragmento nem parâmetros OAuth |
|
|
243
|
+
| `scope` | `profile` | `profile saves` para Cloud Saves (o cadastro do jogo precisa permitir `saves`) |
|
|
244
|
+
| `restoreSession` | `true` | reautoriza após reload da mesma aba sem persistir token |
|
|
245
|
+
| `restoreAcrossSessions` | `false` | opt-in: recupera a conta em aba nova/navegador reaberto via `prompt=none` (indício não secreto em `localStorage`) |
|
|
246
|
+
| `timeoutMs` | `10000` | cobre resposta e leitura do corpo |
|
|
247
|
+
| `credentials` | `omit` | política dos módulos genéricos; Auth força `omit` para token/userinfo/revoke |
|
|
248
|
+
| `fetch` | `globalThis.fetch` | injetável para testes/instrumentação |
|
|
249
|
+
| `browser` | globals do browser | adaptador avançado para webviews e testes |
|
|
250
|
+
| `analytics` | — | opt-in: `{ posthog: { projectToken: 'phc_…', host }, gameVersion }`; sem ele, no-op ([guia](docs/ANALYTICS.md)) |
|
|
178
251
|
|
|
179
252
|
`authUrl` é informativo/configurável para ambientes, mas o SDK nunca concatena nem escolhe um retorno
|
|
180
253
|
via portal: `/v1/oauth/authorize` valida o cliente/callback e redireciona para o portal configurado na
|
|
@@ -202,10 +275,14 @@ uma ferramenta de teste, não UI oficial.
|
|
|
202
275
|
- `crypto.getRandomValues` e Web Crypto SHA-256 geram `state`, verifier e challenge; não há
|
|
203
276
|
`Math.random`.
|
|
204
277
|
- 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.
|
|
278
|
+
`sessionStorage`, cookies, URL, estado observável ou mensagens de erro. Com `restoreAcrossSessions`,
|
|
279
|
+
`localStorage` recebe apenas o indício `{ version, scope, at }`.
|
|
206
280
|
- CORS/Origin não são considerados autenticação. PKCE, URI exata, state e vínculos persistidos pela API
|
|
207
281
|
são os controles de segurança.
|
|
208
282
|
- XSS ainda pode ler um token que esteja em memória; jogos devem manter CSP e dependências sob controle.
|
|
283
|
+
- Analytics só transmite após consentimento explícito, usa o token **público** de projeto (`phc_…`; o SDK
|
|
284
|
+
recusa `phx_…`) e nunca recebe tokens, e-mail, nome, dados de save ou URLs com query string. O
|
|
285
|
+
identificador enviado após login é o `player.id` interno.
|
|
209
286
|
- Cloud Saves sempre passam pela autorização do backend (token do próprio jogo + escopo `saves`); o SDK
|
|
210
287
|
nunca envia `playerId`, e a UI nunca prova legitimidade de progresso.
|
|
211
288
|
|
|
@@ -253,7 +330,8 @@ npm run verify
|
|
|
253
330
|
npm run example:smoke # leitura opcional da API real
|
|
254
331
|
```
|
|
255
332
|
|
|
256
|
-
Mais detalhes: [
|
|
333
|
+
Mais detalhes: [analytics](docs/ANALYTICS.md), [renovação de sessão](docs/SESSION_RENEWAL.md),
|
|
334
|
+
[Cloud Saves](docs/CLOUD_SAVES.md),
|
|
257
335
|
[auditoria da Fase 4](docs/PHASE_04_CONTRACT_AUDIT.md), [widget oficial](docs/phase-03-widget.md),
|
|
258
336
|
[integração SSO](docs/SSO_INTEGRATION.md),
|
|
259
337
|
[auditoria do contrato](docs/SSO_CONTRACT_AUDIT.md), [arquitetura](docs/architecture.md) e
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PixelumeAnalyticsApi } from '../modules/analytics/AnalyticsModule.js';
|
|
1
2
|
import { type PixelumeAuthApi, type PixelumeAuthState } from '../modules/auth/AuthModule.js';
|
|
2
3
|
import { type PixelumeHealthApi } from '../modules/health/HealthModule.js';
|
|
3
4
|
import { type PixelumeSavesApi } from '../modules/saves/SavesModule.js';
|
|
@@ -10,7 +11,7 @@ import type { PixelumeClientConfig, PixelumeClientOptions } from './types.js';
|
|
|
10
11
|
* const health = await pixelume.health.check();
|
|
11
12
|
* ```
|
|
12
13
|
*
|
|
13
|
-
* Feature modules are exposed as read-only properties: `health`, `auth` and `
|
|
14
|
+
* Feature modules are exposed as read-only properties: `health`, `auth`, `saves` and `analytics`. New modules are
|
|
14
15
|
* added as new properties without changing existing ones. Nothing touches the DOM; the visual
|
|
15
16
|
* widget lives in the opt-in `@pixelume/sdk/ui` entry point.
|
|
16
17
|
*
|
|
@@ -23,6 +24,11 @@ export declare class PixelumeClient {
|
|
|
23
24
|
readonly auth: PixelumeAuthApi;
|
|
24
25
|
/** Cloud Saves of the signed-in player for `gameId` (requires the `saves` scope). */
|
|
25
26
|
readonly saves: PixelumeSavesApi;
|
|
27
|
+
/**
|
|
28
|
+
* Gameplay analytics. A no-op unless the `analytics` option is configured, and silent until the
|
|
29
|
+
* player consents (`analytics.setConsent(true)`).
|
|
30
|
+
*/
|
|
31
|
+
readonly analytics: PixelumeAnalyticsApi;
|
|
26
32
|
constructor(options?: PixelumeClientOptions);
|
|
27
33
|
/**
|
|
28
34
|
* Initializes browser authentication once. Callback failures become an anonymous state so a
|
|
@@ -32,7 +38,9 @@ export declare class PixelumeClient {
|
|
|
32
38
|
/**
|
|
33
39
|
* Stops renewal timers and resume listeners and drops credentials from memory, without
|
|
34
40
|
* 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.
|
|
41
|
+
* `auth.logout()` first to also revoke the authorization. Analytics stops delivering (an active
|
|
42
|
+
* game session is not ended; call `analytics.endSession()` first if play really ended). The
|
|
43
|
+
* client is unusable afterwards.
|
|
36
44
|
*/
|
|
37
45
|
destroy(): void;
|
|
38
46
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { HttpClient } from '../http/HttpClient.js';
|
|
2
|
+
import { AnalyticsModule, } from '../modules/analytics/AnalyticsModule.js';
|
|
2
3
|
import { AuthModule, } from '../modules/auth/AuthModule.js';
|
|
3
4
|
import { HealthModule } from '../modules/health/HealthModule.js';
|
|
4
5
|
import { SavesModule } from '../modules/saves/SavesModule.js';
|
|
@@ -11,7 +12,7 @@ import { resolveConfig } from './config.js';
|
|
|
11
12
|
* const health = await pixelume.health.check();
|
|
12
13
|
* ```
|
|
13
14
|
*
|
|
14
|
-
* Feature modules are exposed as read-only properties: `health`, `auth` and `
|
|
15
|
+
* Feature modules are exposed as read-only properties: `health`, `auth`, `saves` and `analytics`. New modules are
|
|
15
16
|
* added as new properties without changing existing ones. Nothing touches the DOM; the visual
|
|
16
17
|
* widget lives in the opt-in `@pixelume/sdk/ui` entry point.
|
|
17
18
|
*
|
|
@@ -23,7 +24,13 @@ export class PixelumeClient {
|
|
|
23
24
|
auth;
|
|
24
25
|
/** Cloud Saves of the signed-in player for `gameId` (requires the `saves` scope). */
|
|
25
26
|
saves;
|
|
27
|
+
/**
|
|
28
|
+
* Gameplay analytics. A no-op unless the `analytics` option is configured, and silent until the
|
|
29
|
+
* player consents (`analytics.setConsent(true)`).
|
|
30
|
+
*/
|
|
31
|
+
analytics;
|
|
26
32
|
#authModule;
|
|
33
|
+
#analyticsModule;
|
|
27
34
|
constructor(options = {}) {
|
|
28
35
|
const resolved = resolveConfig(options);
|
|
29
36
|
this.config = Object.freeze({
|
|
@@ -34,8 +41,10 @@ export class PixelumeClient {
|
|
|
34
41
|
scope: resolved.scope,
|
|
35
42
|
authUrl: resolved.authUrl,
|
|
36
43
|
restoreSession: resolved.restoreSession,
|
|
44
|
+
restoreAcrossSessions: resolved.crossSessionRestore !== null,
|
|
37
45
|
timeoutMs: resolved.timeoutMs,
|
|
38
46
|
credentials: resolved.credentials,
|
|
47
|
+
analytics: resolved.analytics === null ? null : resolved.analytics.provider.kind,
|
|
39
48
|
});
|
|
40
49
|
const http = new HttpClient({
|
|
41
50
|
baseUrl: resolved.apiUrl,
|
|
@@ -47,6 +56,12 @@ export class PixelumeClient {
|
|
|
47
56
|
this.#authModule = new AuthModule(http, resolved);
|
|
48
57
|
this.auth = this.#authModule;
|
|
49
58
|
this.saves = new SavesModule(this.#authModule.sessionManager, resolved);
|
|
59
|
+
this.#analyticsModule = new AnalyticsModule({
|
|
60
|
+
config: resolved.analytics,
|
|
61
|
+
auth: this.#authModule,
|
|
62
|
+
browser: resolved.browser,
|
|
63
|
+
});
|
|
64
|
+
this.analytics = this.#analyticsModule;
|
|
50
65
|
}
|
|
51
66
|
/**
|
|
52
67
|
* Initializes browser authentication once. Callback failures become an anonymous state so a
|
|
@@ -58,9 +73,12 @@ export class PixelumeClient {
|
|
|
58
73
|
/**
|
|
59
74
|
* Stops renewal timers and resume listeners and drops credentials from memory, without
|
|
60
75
|
* 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.
|
|
76
|
+
* `auth.logout()` first to also revoke the authorization. Analytics stops delivering (an active
|
|
77
|
+
* game session is not ended; call `analytics.endSession()` first if play really ended). The
|
|
78
|
+
* client is unusable afterwards.
|
|
62
79
|
*/
|
|
63
80
|
destroy() {
|
|
81
|
+
this.#analyticsModule.dispose();
|
|
64
82
|
this.#authModule.dispose();
|
|
65
83
|
}
|
|
66
84
|
}
|
|
@@ -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;
|
|
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,eAAe,GAEhB,MAAM,yCAAyC,CAAC;AACjD,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;IACjC;;;OAGG;IACM,SAAS,CAAuB;IAChC,WAAW,CAAa;IACxB,gBAAgB,CAAkB;IAE3C,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;YACjC,SAAS,EAAE,QAAQ,CAAC,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI;SACjF,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;QACxE,IAAI,CAAC,gBAAgB,GAAG,IAAI,eAAe,CAAC;YAC1C,MAAM,EAAE,QAAQ,CAAC,SAAS;YAC1B,IAAI,EAAE,IAAI,CAAC,WAAW;YACtB,OAAO,EAAE,QAAQ,CAAC,OAAO;SAC1B,CAAC,CAAC;QACH,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,gBAAgB,CAAC;IACzC,CAAC;IAED;;;OAGG;IACH,UAAU;QACR,OAAO,IAAI,CAAC,WAAW,CAAC,UAAU,EAAE,CAAC;IACvC,CAAC;IAED;;;;;;OAMG;IACH,OAAO;QACL,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC;QAChC,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 AnalyticsModule,\n type PixelumeAnalyticsApi,\n} from '../modules/analytics/AnalyticsModule.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`, `saves` and `analytics`. 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 /**\n * Gameplay analytics. A no-op unless the `analytics` option is configured, and silent until the\n * player consents (`analytics.setConsent(true)`).\n */\n readonly analytics: PixelumeAnalyticsApi;\n readonly #authModule: AuthModule;\n readonly #analyticsModule: AnalyticsModule;\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 analytics: resolved.analytics === null ? null : resolved.analytics.provider.kind,\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 this.#analyticsModule = new AnalyticsModule({\n config: resolved.analytics,\n auth: this.#authModule,\n browser: resolved.browser,\n });\n this.analytics = this.#analyticsModule;\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. Analytics stops delivering (an active\n * game session is not ended; call `analytics.endSession()` first if play really ended). The\n * client is unusable afterwards.\n */\n destroy(): void {\n this.#analyticsModule.dispose();\n this.#authModule.dispose();\n }\n}\n\n/** Concise alias for new integrations; `PixelumeClient` remains supported. */\nexport { PixelumeClient as Pixelume };\n"]}
|
package/dist/core/config.d.ts
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import type { CredentialsPolicy, FetchLike } from '../http/types.js';
|
|
2
|
-
import type { PixelumeClientOptions } from './types.js';
|
|
2
|
+
import type { PixelumeAnalyticsIssue, PixelumeClientOptions } from './types.js';
|
|
3
3
|
export declare const DEFAULT_API_URL = "https://api.pixelume.com.br";
|
|
4
4
|
export declare const DEFAULT_AUTH_URL = "https://auth.pixelume.com.br";
|
|
5
5
|
export declare const DEFAULT_TIMEOUT_MS = 10000;
|
|
6
|
+
/** Validated analytics configuration. `null` in `ResolvedConfig` means "no-op". */
|
|
7
|
+
export interface ResolvedAnalyticsConfig {
|
|
8
|
+
readonly provider: {
|
|
9
|
+
readonly kind: 'posthog';
|
|
10
|
+
readonly token: string;
|
|
11
|
+
readonly host: string;
|
|
12
|
+
};
|
|
13
|
+
readonly gameId: string;
|
|
14
|
+
readonly gameVersion: string | null;
|
|
15
|
+
readonly rememberConsent: boolean;
|
|
16
|
+
readonly onIssue: ((issue: PixelumeAnalyticsIssue) => void) | null;
|
|
17
|
+
}
|
|
6
18
|
export interface ResolvedConfig {
|
|
7
19
|
apiUrl: string;
|
|
8
20
|
gameId: string | null;
|
|
@@ -11,11 +23,16 @@ export interface ResolvedConfig {
|
|
|
11
23
|
scope: 'profile' | 'profile saves';
|
|
12
24
|
authUrl: string;
|
|
13
25
|
restoreSession: boolean;
|
|
26
|
+
/** `null` when cross-session restoration is disabled. */
|
|
27
|
+
crossSessionRestore: {
|
|
28
|
+
readonly maxAgeMs: number;
|
|
29
|
+
} | null;
|
|
14
30
|
allowInsecureHttp: boolean;
|
|
15
31
|
browser: PixelumeClientOptions['browser'];
|
|
16
32
|
timeoutMs: number;
|
|
17
33
|
credentials: CredentialsPolicy;
|
|
18
34
|
fetch: FetchLike;
|
|
35
|
+
analytics: ResolvedAnalyticsConfig | null;
|
|
19
36
|
}
|
|
20
37
|
export declare function resolveConfig(options: PixelumeClientOptions): ResolvedConfig;
|
|
21
38
|
/**
|
package/dist/core/config.js
CHANGED
|
@@ -6,6 +6,8 @@ const MAX_TIMEOUT_MS = 2_147_483_647; // setTimeout limit
|
|
|
6
6
|
const GAME_ID_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/u;
|
|
7
7
|
const CLIENT_ID_PATTERN = /^[A-Za-z0-9._-]{1,128}$/u;
|
|
8
8
|
const CREDENTIALS = ['omit', 'same-origin', 'include'];
|
|
9
|
+
const POSTHOG_PROJECT_TOKEN_PATTERN = /^phc_[A-Za-z0-9_-]{8,128}$/u;
|
|
10
|
+
const GAME_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$/u;
|
|
9
11
|
export function resolveConfig(options) {
|
|
10
12
|
const allowInsecureHttp = options.allowInsecureHttp === true;
|
|
11
13
|
const gameId = options.gameId === undefined ? null : validateGameId(options.gameId);
|
|
@@ -17,13 +19,93 @@ export function resolveConfig(options) {
|
|
|
17
19
|
redirectUri: resolveRedirectUri(options, allowInsecureHttp),
|
|
18
20
|
scope: validateScope(options.scope ?? 'profile'),
|
|
19
21
|
restoreSession: validateBoolean('restoreSession', options.restoreSession ?? true),
|
|
22
|
+
crossSessionRestore: resolveCrossSessionRestore(options.restoreAcrossSessions),
|
|
20
23
|
allowInsecureHttp,
|
|
21
24
|
browser: options.browser,
|
|
22
25
|
timeoutMs: options.timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : validateTimeout(options.timeoutMs),
|
|
23
26
|
credentials: validateCredentials(options.credentials ?? 'omit'),
|
|
24
27
|
fetch: resolveFetch(options.fetch),
|
|
28
|
+
analytics: resolveAnalytics(options.analytics, gameId, allowInsecureHttp),
|
|
25
29
|
};
|
|
26
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Missing configuration (no object, `enabled: false`, empty token or host) is a no-op, so a build
|
|
33
|
+
* without the variables keeps working. A value that is present but wrong fails fast, like every
|
|
34
|
+
* other option; a personal API key is always refused.
|
|
35
|
+
*/
|
|
36
|
+
function resolveAnalytics(value, gameId, allowInsecureHttp) {
|
|
37
|
+
if (value === undefined)
|
|
38
|
+
return null;
|
|
39
|
+
// Validated at runtime: JavaScript callers may pass anything.
|
|
40
|
+
const raw = value;
|
|
41
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
42
|
+
throw new PixelumeConfigError('analytics must be an object');
|
|
43
|
+
}
|
|
44
|
+
const enabled = validateBoolean('analytics.enabled', value.enabled ?? true);
|
|
45
|
+
const provider = value.provider ?? 'posthog';
|
|
46
|
+
if (provider !== 'posthog')
|
|
47
|
+
throw new PixelumeConfigError('analytics.provider must be "posthog"');
|
|
48
|
+
const rememberConsent = validateBoolean('analytics.rememberConsent', value.rememberConsent ?? true);
|
|
49
|
+
const onIssue = value.onIssue;
|
|
50
|
+
if (onIssue !== undefined && typeof onIssue !== 'function') {
|
|
51
|
+
throw new PixelumeConfigError('analytics.onIssue must be a function');
|
|
52
|
+
}
|
|
53
|
+
// Like token and host, an empty value (build variable defined but blank) means "not provided".
|
|
54
|
+
const rawGameVersion = optionalString('analytics.gameVersion', value.gameVersion);
|
|
55
|
+
const gameVersion = rawGameVersion === null ? null : validateGameVersion(rawGameVersion);
|
|
56
|
+
const posthog = value.posthog;
|
|
57
|
+
if (posthog !== undefined && (typeof posthog !== 'object' || posthog === null)) {
|
|
58
|
+
throw new PixelumeConfigError('analytics.posthog must be an object');
|
|
59
|
+
}
|
|
60
|
+
const token = optionalString('analytics.posthog.projectToken', value.posthog?.projectToken);
|
|
61
|
+
const host = optionalString('analytics.posthog.host', value.posthog?.host);
|
|
62
|
+
if (token !== null)
|
|
63
|
+
validatePostHogToken(token);
|
|
64
|
+
const normalizedHost = host === null ? null : normalizeAnalyticsHost(host, allowInsecureHttp);
|
|
65
|
+
if (!enabled || token === null || normalizedHost === null)
|
|
66
|
+
return null;
|
|
67
|
+
if (gameId === null) {
|
|
68
|
+
throw new PixelumeConfigError('analytics requires the client to be created with a gameId');
|
|
69
|
+
}
|
|
70
|
+
return Object.freeze({
|
|
71
|
+
provider: Object.freeze({ kind: 'posthog', token, host: normalizedHost }),
|
|
72
|
+
gameId,
|
|
73
|
+
gameVersion,
|
|
74
|
+
rememberConsent,
|
|
75
|
+
onIssue: onIssue ?? null,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function optionalString(name, value) {
|
|
79
|
+
if (value === undefined || value === null)
|
|
80
|
+
return null;
|
|
81
|
+
if (typeof value !== 'string')
|
|
82
|
+
throw new PixelumeConfigError(`${name} must be a string`);
|
|
83
|
+
const trimmed = value.trim();
|
|
84
|
+
return trimmed === '' ? null : trimmed;
|
|
85
|
+
}
|
|
86
|
+
function validatePostHogToken(token) {
|
|
87
|
+
if (/^phx_/u.test(token)) {
|
|
88
|
+
throw new PixelumeConfigError('analytics.posthog.projectToken must be the public project token (phc_...); personal API keys (phx_...) must never reach the browser');
|
|
89
|
+
}
|
|
90
|
+
if (!POSTHOG_PROJECT_TOKEN_PATTERN.test(token)) {
|
|
91
|
+
throw new PixelumeConfigError('analytics.posthog.projectToken must be a PostHog project token (phc_...)');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Same rules as `apiUrl`; a path is allowed for reverse proxies (e.g. `https://example.com/ingest`). */
|
|
95
|
+
function normalizeAnalyticsHost(value, allowInsecureHttp) {
|
|
96
|
+
try {
|
|
97
|
+
return normalizeApiUrl(value, allowInsecureHttp);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
throw new PixelumeConfigError('analytics.posthog.host must be an https: URL without credentials, query or fragment, e.g. https://us.i.posthog.com');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function validateGameVersion(value) {
|
|
104
|
+
if (typeof value !== 'string' || !GAME_VERSION_PATTERN.test(value)) {
|
|
105
|
+
throw new PixelumeConfigError('analytics.gameVersion must be 1-64 letters, digits, dots, plus signs, underscores or hyphens');
|
|
106
|
+
}
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
27
109
|
function resolveRedirectUri(options, allowInsecureHttp) {
|
|
28
110
|
const value = options.redirectUri ?? defaultRedirectUri(options.browser);
|
|
29
111
|
return value === null ? null : normalizeRedirectUri(value, allowInsecureHttp);
|
|
@@ -34,6 +116,23 @@ function validateScope(value) {
|
|
|
34
116
|
}
|
|
35
117
|
return value;
|
|
36
118
|
}
|
|
119
|
+
const DAY_MS = 24 * 60 * 60_000;
|
|
120
|
+
function resolveCrossSessionRestore(value) {
|
|
121
|
+
if (value === undefined || value === false)
|
|
122
|
+
return null;
|
|
123
|
+
if (value === true)
|
|
124
|
+
return Object.freeze({ maxAgeMs: 30 * DAY_MS });
|
|
125
|
+
// Validated at runtime: JavaScript callers may pass anything.
|
|
126
|
+
const raw = value;
|
|
127
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
128
|
+
throw new PixelumeConfigError('restoreAcrossSessions must be a boolean or { maxAgeDays }');
|
|
129
|
+
}
|
|
130
|
+
const days = raw.maxAgeDays ?? 30;
|
|
131
|
+
if (typeof days !== 'number' || !Number.isInteger(days) || days < 1 || days > 90) {
|
|
132
|
+
throw new PixelumeConfigError('restoreAcrossSessions.maxAgeDays must be an integer from 1 to 90');
|
|
133
|
+
}
|
|
134
|
+
return Object.freeze({ maxAgeMs: days * DAY_MS });
|
|
135
|
+
}
|
|
37
136
|
function validateBoolean(name, value) {
|
|
38
137
|
if (typeof value !== 'boolean')
|
|
39
138
|
throw new PixelumeConfigError(`${name} must be a boolean`);
|