@pixelume/sdk 0.1.1 → 0.2.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,24 @@ conter mudanças incompatíveis, sempre listadas em **Changed/Removed**.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2026-10-13
11
+
12
+ ### Added
13
+
14
+ - `Pixelume` como alias conciso e `PixelumeClient.initialize()` idempotente.
15
+ - `client.auth` com Authorization Code + PKCE S256: login por redirecionamento, callback validado,
16
+ consulta de jogador, revogação/logout, estado observável e assinatura cancelável.
17
+ - Restauração após recarga por nova autorização SSO, sem persistir access token; somente um marcador
18
+ não sensível e o estado PKCE transitório ficam em `sessionStorage`.
19
+ - Configuração tipada de `clientId`, `redirectUri`, `authUrl`, `restoreSession` e adaptador de browser.
20
+ - Exemplo Vite com fluxo de autenticação completo e auditoria/documentação do contrato real.
21
+
22
+ ### Changed
23
+
24
+ - O `kind` de `PixelumeError` ganhou o valor aditivo `auth`; consumidores `0.x` com `switch`
25
+ exaustivo devem tratar esse novo caso.
26
+ - O cliente HTTP interno passou a serializar formulários OAuth URL-encoded.
27
+
10
28
  ## [0.1.1] - 2026-10-13
11
29
 
12
30
  ### Changed
@@ -29,6 +47,7 @@ conter mudanças incompatíveis, sempre listadas em **Changed/Removed**.
29
47
  `invalid_response`) e `isPixelumeError()`.
30
48
  - Build ESM + `.d.ts`, CI e workflow de publicação via npm Trusted Publishing.
31
49
 
32
- [Unreleased]: https://github.com/ericksonnascimento/pixelume-sdk/compare/v0.1.1...HEAD
50
+ [Unreleased]: https://github.com/ericksonnascimento/pixelume-sdk/compare/v0.2.0...HEAD
51
+ [0.2.0]: https://github.com/ericksonnascimento/pixelume-sdk/compare/v0.1.1...v0.2.0
33
52
  [0.1.1]: https://github.com/ericksonnascimento/pixelume-sdk/compare/v0.1.0...v0.1.1
34
53
  [0.1.0]: https://github.com/ericksonnascimento/pixelume-sdk/releases/tag/v0.1.0
package/README.md CHANGED
@@ -1,27 +1,8 @@
1
1
  # @pixelume/sdk
2
2
 
3
- SDK TypeScript dos jogos web do estúdio **Pixelume**. Fornece um cliente tipado, seguro e sem
4
- dependências de runtime para a [Pixelume API](https://api.pixelume.com.br), independente de framework ou
5
- engine (funciona com Vite, Three.js, React, JavaScript puro, SSR e workers).
6
-
7
- > **Status: Fase 1 — fundação.** Esta versão oferece configuração, cliente HTTP, erros padronizados e a
8
- > checagem de disponibilidade da API. Login, sessão, cloud saves e analytics **ainda não existem** e
9
- > serão adicionados quando os contratos da API estiverem prontos (veja o [roadmap](docs/roadmap.md)).
10
-
11
- ## Capacidades
12
-
13
- | Capacidade | Fase 1 | Observação |
14
- | -------------------------------------------- | :----: | ------------------------------------------------------- |
15
- | `PixelumeClient` com configuração validada | ✅ | `apiUrl`, `gameId`, `timeoutMs`, `credentials`, `fetch` |
16
- | `client.health.check()` (`GET /health`) | ✅ | sem CORS na API: servidor/mesma origem (ver abaixo) |
17
- | Erros padronizados, timeout e cancelamento | ✅ | `PixelumeError` + `kind` |
18
- | Autenticação / Pixelume Auth | — | aguarda portal central e contratos de redirect |
19
- | Cloud saves, analytics, rankings, conquistas | — | aguardam contratos na API |
20
-
21
- ## Pré-requisitos
22
-
23
- - Navegadores modernos (Chrome/Edge 98+, Firefox 97+, Safari 15.4+) ou Node.js 22+, Deno, Bun, Workers.
24
- - Projeto ESM (o pacote é ESM-only, com tipos incluídos). Em CommonJS use `await import('@pixelume/sdk')`.
3
+ SDK TypeScript headless para jogos web da Pixelume. A versão 0.2 adiciona autenticação centralizada
4
+ com OAuth 2.0 Authorization Code + PKCE S256, além do cliente HTTP e health check da Fase 1. Não há
5
+ dependências de runtime, componentes visuais, senha no jogo nem `client_secret`.
25
6
 
26
7
  ## Instalação
27
8
 
@@ -29,91 +10,149 @@ engine (funciona com Vite, Three.js, React, JavaScript puro, SSR e workers).
29
10
  npm install @pixelume/sdk
30
11
  ```
31
12
 
32
- Publicado no npm: [`@pixelume/sdk`](https://www.npmjs.com/package/@pixelume/sdk) (versão atual `0.1.0`).
13
+ O pacote é ESM-only, inclui tipos e funciona em Vite, JavaScript/TypeScript puro e engines web. Em
14
+ CommonJS, use `await import('@pixelume/sdk')`.
33
15
 
34
- ## Uso
16
+ ## Configuração obrigatória no backend
35
17
 
36
- ```ts
37
- import { PixelumeClient } from '@pixelume/sdk';
18
+ Antes de integrar um jogo, registre na Pixelume API:
38
19
 
39
- const pixelume = new PixelumeClient({
40
- apiUrl: 'https://api.pixelume.com.br', // padrão
41
- gameId: 'rail-puzzle', // opcional nesta fase
42
- });
20
+ - um `client_id` público, normalmente igual ao `gameId`;
21
+ - o escopo `profile`;
22
+ - cada callback completo por igualdade exata de scheme, host, porta, path e query.
43
23
 
44
- const health = await pixelume.health.check();
45
- // { status: 'ok', registration: 'email-verification', release: 'd99d195…' }
46
- ```
24
+ Não são aceitos curingas, prefixos ou callbacks enviados dinamicamente. O cliente local já cadastrado é
25
+ `pixelume-local-dev`, exclusivamente com `http://localhost:4173/oauth/callback` e
26
+ `http://127.0.0.1:4173/oauth/callback`. Jogos publicados exigem migration/configuração revisada na API.
47
27
 
48
- Saída real obtida pelo exemplo em 2026-10-13 (Node.js, pacote instalado via `npm pack`):
28
+ ## Uso
49
29
 
50
- ```json
51
- {
52
- "sdk": "0.1.0",
53
- "apiUrl": "https://api.pixelume.com.br",
54
- "health": {
55
- "status": "ok",
56
- "registration": "email-verification",
57
- "release": "d99d19543620f09309268f67d959e874ddc78ecf"
58
- }
59
- }
60
- ```
30
+ ```ts
31
+ import { Pixelume, isPixelumeError } from '@pixelume/sdk';
32
+
33
+ const pixelume = new Pixelume({
34
+ gameId: 'rail-puzzle',
35
+ // Use clientId somente quando for diferente do gameId.
36
+ // clientId: 'rail-puzzle.web',
37
+ // Padrão no browser: `${location.origin}/oauth/callback`.
38
+ redirectUri: 'https://rail-puzzle.pixelume.com.br/oauth/callback',
39
+ });
61
40
 
62
- ### `health.check()` no navegador
41
+ const initial = await pixelume.initialize();
42
+ // `initialize()` nunca impede o jogo de abrir: falhas de callback viram estado anonymous/error.
63
43
 
64
- A API ainda **não envia cabeçalhos CORS em `/health`** (só em `/v1/*`). Em uma página servida por outra
65
- origem, o navegador bloqueia a resposta e o SDK rejeita com `PixelumeNetworkError`. Use `health.check()`
66
- no servidor (Node/SSR/workers), em ferramentas, ou na mesma origem via proxy — o
67
- [exemplo Vite](examples/vite-basic) usa o proxy do `vite dev`. A correção depende da API
68
- ([auditoria](docs/api-contract-audit.md)).
44
+ const unsubscribe = pixelume.auth.subscribe((state, previous) => {
45
+ if (state.status !== previous.status) renderAccountState(state);
46
+ });
69
47
 
70
- ### Configuração
48
+ const player = await pixelume.auth.getPlayer(); // PixelumePlayer | null
71
49
 
72
- | Opção | Padrão | Descrição |
73
- | ------------------- | ----------------------------- | -------------------------------------------------------------------------------------------- |
74
- | `apiUrl` | `https://api.pixelume.com.br` | `https:` obrigatório; `http:` aceito só para `localhost`/`127.0.0.1`/`[::1]`/`*.localhost`. |
75
- | `gameId` | — | slug público (`a-z`, `0-9`, `-`, até 64). Identifica o jogo; **não** é credencial. |
76
- | `timeoutMs` | `10000` | tempo máximo de cada operação, incluindo leitura do corpo. |
77
- | `credentials` | `'omit'` | política de cookies do `fetch` para módulos futuros. O SDK não presume cookies cross-origin. |
78
- | `fetch` | `globalThis.fetch` | implementação injetável (testes, SSR, instrumentação). |
79
- | `allowInsecureHttp` | `false` | permite `http:` em hosts não-loopback (ex.: IP da LAN). Nunca em produção. |
50
+ loginButton.addEventListener('click', () => {
51
+ void pixelume.auth.login(); // navegação de topo para a API e o portal Auth
52
+ });
80
53
 
81
- Opções inválidas lançam `PixelumeConfigError` no construtor. Cada operação aceita
82
- `{ signal?: AbortSignal; timeoutMs?: number }`.
54
+ logoutButton.addEventListener('click', async () => {
55
+ try {
56
+ await pixelume.auth.logout(); // revoga somente o token deste jogo
57
+ } catch (error) {
58
+ if (!isPixelumeError(error)) throw error;
59
+ showRecoverableError(error.kind);
60
+ }
61
+ });
83
62
 
84
- ### Erros
63
+ // Ao desmontar a tela:
64
+ unsubscribe();
65
+ ```
85
66
 
86
- Todas as falhas são `PixelumeError` com um `kind` estável:
67
+ `pixelume.auth.state.status` é `uninitialized`, `redirecting`, `exchanging`, `anonymous` ou
68
+ `authenticated`. O estado anônimo informa `reason` (`visitor`, `cancelled`, `expired`, `logged_out` ou
69
+ `error`) e, em falha, um `PixelumeError`. Cancelar no portal resulta em visitante, não em exceção.
70
+
71
+ ### Visitante, recarga e expiração
72
+
73
+ - Sem login ou callback, `initialize()` termina como visitante e não faz requisição.
74
+ - O access token opaco dura 10 minutos e fica **somente em memória**.
75
+ - Após autenticar, o SDK guarda em `sessionStorage` apenas o marcador não secreto de que aquela aba
76
+ tinha uma sessão. Em uma recarga, inicia uma nova autorização; enquanto a sessão central da API
77
+ continuar válida, o portal retorna sem pedir senha. `restoreSession: false` desliga esse comportamento.
78
+ - PKCE `state`/`code_verifier` ficam na sessão por no máximo 10 minutos e são removidos antes da troca.
79
+ - Código expirado/repetido, `state` divergente, callback adulterado e 401 deixam o jogo utilizável como
80
+ visitante. O jogo pode oferecer o login novamente.
81
+ - `logout()` limpa o estado local imediatamente e chama `/v1/oauth/revoke`. A sessão central do portal
82
+ continua existente por desenho; entrar novamente pode ser imediato.
83
+
84
+ ## Configuração
85
+
86
+ | Opção | Padrão | Observação |
87
+ | ---------------- | ------------------------------------ | ----------------------------------------------------------------------------------------- |
88
+ | `apiUrl` | `https://api.pixelume.com.br` | HTTPS; HTTP só em loopback ou com `allowInsecureHttp` explícito |
89
+ | `authUrl` | `https://auth.pixelume.com.br` | Portal esperado; a API continua sendo a autoridade que escolhe o redirect |
90
+ | `gameId` | — | slug público do jogo; também é o `clientId` padrão |
91
+ | `clientId` | `gameId` | identificador OAuth público cadastrado; nunca é credencial |
92
+ | `redirectUri` | `<origin>/oauth/callback` no browser | deve coincidir exatamente com o cadastro e não pode conter fragmento nem parâmetros OAuth |
93
+ | `restoreSession` | `true` | reautoriza após reload da mesma aba sem persistir token |
94
+ | `timeoutMs` | `10000` | cobre resposta e leitura do corpo |
95
+ | `credentials` | `omit` | política dos módulos genéricos; Auth força `omit` para token/userinfo/revoke |
96
+ | `fetch` | `globalThis.fetch` | injetável para testes/instrumentação |
97
+ | `browser` | globals do browser | adaptador avançado para webviews e testes |
98
+
99
+ `authUrl` é informativo/configurável para ambientes, mas o SDK nunca concatena nem escolhe um retorno
100
+ via portal: `/v1/oauth/authorize` valida o cliente/callback e redireciona para o portal configurado na
101
+ API. Isso evita transformar o SDK em fonte de open redirect.
102
+
103
+ ## Desenvolvimento local e produção
104
+
105
+ | Ambiente | Cliente/callback | API/portal |
106
+ | ---------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------- |
107
+ | exemplo local | `pixelume-local-dev` + `http://localhost:4173/oauth/callback` | produção por padrão; callbacks loopback já cadastrados |
108
+ | API/Auth locais | cadastrar o mesmo callback no banco local e configurar as origens do portal/API | `apiUrl` pode ser loopback; o portal é escolhido pela API local |
109
+ | jogo em produção | cliente próprio + callback HTTPS exato | `https://api.pixelume.com.br` → `https://auth.pixelume.com.br` |
110
+
111
+ Execute `npm run example:prepare`, `npm run example:build` e depois
112
+ `npm --prefix examples/vite-basic run dev`. O exemplo tem botões de login, consulta e logout e é apenas
113
+ uma ferramenta de teste, não UI oficial.
114
+
115
+ ## Segurança e confiança
116
+
117
+ - O fluxo não é OIDC: não há ID Token, discovery, refresh token nem `client_secret`.
118
+ - A sessão central é cookie `HttpOnly`, `Secure`, `SameSite=Lax` e host-only da API. O jogo nunca a lê.
119
+ - Requests OAuth do jogo usam Bearer curto e `credentials: 'omit'`; `credentials: 'include'` pertence
120
+ apenas ao portal ao consultar/concluir a sessão central.
121
+ - `crypto.getRandomValues` e Web Crypto SHA-256 geram `state`, verifier e challenge; não há
122
+ `Math.random`.
123
+ - Access token, código e verifier não são registrados. Tokens não vão para `localStorage`, cookies ou URL.
124
+ - CORS/Origin não são considerados autenticação. PKCE, URI exata, state e vínculos persistidos pela API
125
+ são os controles de segurança.
126
+ - XSS ainda pode ler um token que esteja em memória; jogos devem manter CSP e dependências sob controle.
127
+
128
+ ## Erros
129
+
130
+ Todo erro lançado diretamente pelo SDK é `PixelumeError`. Os `kind` estáveis são `config`, `auth`,
131
+ `http`, `network`, `timeout`, `aborted` e `invalid_response`. `initialize()` captura falhas para permitir
132
+ o modo visitante; consulte `pixelume.auth.state.error`. Métodos chamados explicitamente (`login`,
133
+ `getPlayer`, `logout`) rejeitam normalmente.
134
+
135
+ O SDK não faz retry automático. Para 400/401/403/429/5xx, use `PixelumeHttpError.status`, `code` e
136
+ `requestId`; para cancelamento do chamador, use `AbortSignal`.
137
+
138
+ ## Health check
87
139
 
88
140
  ```ts
89
- import { isPixelumeError } from '@pixelume/sdk';
90
-
91
- const controller = new AbortController();
92
- try {
93
- await pixelume.health.check({ signal: controller.signal, timeoutMs: 5_000 });
94
- } catch (error) {
95
- if (!isPixelumeError(error)) throw error;
96
- switch (error.kind) {
97
- case 'http': // error.status, error.code ('RATE_LIMITED'…), error.requestId
98
- case 'network': // offline, DNS/TLS ou bloqueio de CORS
99
- case 'timeout': // error.timeoutMs
100
- case 'aborted': // cancelado pelo AbortSignal
101
- case 'invalid_response': // a API respondeu fora do contrato
102
- case 'config': // configuração inválida
103
- }
104
- }
141
+ const health = await pixelume.health.check();
105
142
  ```
106
143
 
107
- Mensagens de erro não incluem corpo, cookies, cabeçalhos nem query string. O corpo bruto fica em
108
- `PixelumeHttpError.payload`, propriedade não enumerável (não aparece em `JSON.stringify`). O SDK nunca
109
- escreve no console.
144
+ `GET /health` ainda não expõe CORS em produção, então funciona em Node/SSR/worker, mesma origem ou proxy
145
+ local, mas não diretamente de outro domínio no browser.
146
+
147
+ ## Compatibilidade
110
148
 
111
- ## Segurança
149
+ | SDK | Pixelume API | Pixelume Auth | Recursos |
150
+ | ------- | ---------------------------------------------------- | ------------------------------ | -------------------------------- |
151
+ | `0.2.x` | OpenAPI `0.4.0`, release SSO `3846479` ou compatível | Fase 2 `dd5a8e5` ou compatível | SSO PKCE, perfil, logout, health |
152
+ | `0.1.x` | API anterior/atual para `/health` | não consumido | fundação e health apenas |
112
153
 
113
- - O SDK não contém nem aceita API keys, tokens administrativos ou senhas.
114
- - `gameId` é público; autorização sempre vem da API.
115
- - Não há fluxo de login com senha dentro do jogo — e não haverá: o login será feito pelo Pixelume Auth.
116
- - O SDK não define `Origin`/`Referer`/`Cookie` e não trata CORS como segurança.
154
+ Como o pacote ainda está em `0.x`, mudanças incompatíveis usam incremento _minor_ e são listadas no
155
+ [CHANGELOG](CHANGELOG.md).
117
156
 
118
157
  ## Desenvolvimento
119
158
 
@@ -121,26 +160,14 @@ Requer Node.js 24 (`.nvmrc`).
121
160
 
122
161
  ```bash
123
162
  npm ci
124
- npm run lint # ESLint + Prettier
125
- npm run typecheck
126
- npm test # unitários + integração com servidor HTTP local (sem rede)
127
- npm run build # dist/ (ESM + .d.ts + sourcemaps)
128
- npm run check:package # dist browser-safe, conteúdo do tarball, publint, attw
129
- npm run example:prepare && npm run example:build # instala o .tgz no exemplo e compila
130
- npm run example:smoke # exemplo Node contra a API real (rede)
131
- npm run test:live # teste opcional contra a API real (rede)
132
- npm run verify # tudo o que a CI executa
163
+ npm run verify
164
+ npm run example:smoke # leitura opcional da API real
133
165
  ```
134
166
 
135
- ## Documentação
136
-
137
- - [Arquitetura e decisões técnicas](docs/architecture.md)
138
- - [Auditoria de contratos da API](docs/api-contract-audit.md)
139
- - [Publicação, CI/CD e rollback](docs/publishing.md)
140
- - [Roadmap](docs/roadmap.md)
141
- - [Changelog](CHANGELOG.md)
167
+ Mais detalhes: [integração SSO](docs/SSO_INTEGRATION.md),
168
+ [auditoria do contrato](docs/SSO_CONTRACT_AUDIT.md), [arquitetura](docs/architecture.md) e
169
+ [publicação](docs/publishing.md).
142
170
 
143
171
  ## Licença
144
172
 
145
- Proprietária — todos os direitos reservados. Veja [LICENSE](LICENSE). A disponibilidade pública no npm
146
- não concede direitos além dos descritos na licença.
173
+ Proprietária — todos os direitos reservados. Veja [LICENSE](LICENSE).
@@ -16,6 +16,23 @@ export type ParseResult<T> = {
16
16
  readonly reason: string;
17
17
  };
18
18
  export type BodyParser<T> = (body: unknown) => ParseResult<T>;
19
+ export interface PlayerWire {
20
+ id: string;
21
+ displayName: string;
22
+ avatarUrl: string | null;
23
+ }
24
+ export interface PlayerEnvelopeWire {
25
+ player: PlayerWire;
26
+ }
27
+ export declare const parsePlayerEnvelope: BodyParser<PlayerEnvelopeWire>;
28
+ export interface OAuthTokenResponseWire {
29
+ access_token: string;
30
+ token_type: 'Bearer';
31
+ expires_in: number;
32
+ scope: 'profile';
33
+ }
34
+ export declare const parseOAuthTokenResponse: BodyParser<OAuthTokenResponseWire>;
35
+ export declare const parseEmptyResponse: BodyParser<void>;
19
36
  /** `GET /health` → 200. OpenAPI: `{ status: 'ok', registration: enum, release?: /^[a-f0-9]{7,40}$/ }`. */
20
37
  export interface HealthResponseWire {
21
38
  status: 'ok';
@@ -13,6 +13,42 @@ const fail = (reason) => ({ ok: false, reason });
13
13
  function isRecord(value) {
14
14
  return typeof value === 'object' && value !== null && !Array.isArray(value);
15
15
  }
16
+ export const parsePlayerEnvelope = (body) => {
17
+ if (!isRecord(body) || !isRecord(body.player)) {
18
+ return fail('expected a JSON object containing "player"');
19
+ }
20
+ const { id, displayName, avatarUrl } = body.player;
21
+ if (typeof id !== 'string' || id.length === 0)
22
+ return fail('expected "player.id" to be a string');
23
+ if (typeof displayName !== 'string')
24
+ return fail('expected "player.displayName" to be a string');
25
+ if (avatarUrl !== null && typeof avatarUrl !== 'string') {
26
+ return fail('expected "player.avatarUrl" to be a string or null');
27
+ }
28
+ return ok({ player: { id, displayName, avatarUrl } });
29
+ };
30
+ export const parseOAuthTokenResponse = (body) => {
31
+ if (!isRecord(body))
32
+ return fail('expected a JSON object');
33
+ if (typeof body.access_token !== 'string' ||
34
+ !/^[A-Za-z0-9_-]{43,128}$/u.test(body.access_token)) {
35
+ return fail('expected "access_token" to be a base64url token');
36
+ }
37
+ if (body.token_type !== 'Bearer')
38
+ return fail('expected "token_type" to be "Bearer"');
39
+ if (!Number.isInteger(body.expires_in) || body.expires_in <= 0) {
40
+ return fail('expected "expires_in" to be a positive integer');
41
+ }
42
+ if (body.scope !== 'profile')
43
+ return fail('expected "scope" to be "profile"');
44
+ return ok({
45
+ access_token: body.access_token,
46
+ token_type: 'Bearer',
47
+ expires_in: body.expires_in,
48
+ scope: 'profile',
49
+ });
50
+ };
51
+ export const parseEmptyResponse = (body) => body === undefined ? ok(undefined) : fail('expected an empty response body');
16
52
  export const parseHealthResponse = (body) => {
17
53
  if (!isRecord(body))
18
54
  return fail('expected a JSON object');
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/contracts/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAOH,MAAM,EAAE,GAAG,CAAI,KAAQ,EAAkB,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AAClE,MAAM,IAAI,GAAG,CAAC,MAAc,EAAsB,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;AAE7E,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AASD,MAAM,CAAC,MAAM,mBAAmB,GAAmC,CAAC,IAAI,EAAE,EAAE;IAC1E,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC,wBAAwB,CAAC,CAAC;IAC3D,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC,8BAA8B,CAAC,CAAC;IACtE,IAAI,OAAO,IAAI,CAAC,YAAY,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,wCAAwC,CAAC,CAAC;IACjG,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;QACnE,OAAO,IAAI,CAAC,gDAAgD,CAAC,CAAC;IAChE,CAAC;IACD,OAAO,EAAE,CAAC;QACR,MAAM,EAAE,IAAI;QACZ,YAAY,EAAE,IAAI,CAAC,YAAY;QAC/B,GAAG,CAAC,IAAI,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;KACjE,CAAC,CAAC;AACL,CAAC,CAAC;AAOF,qGAAqG;AACrG,MAAM,UAAU,kBAAkB,CAAC,IAAa;IAC9C,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1D,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC;IAChD,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE,CAAC;QAC7F,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,CAAC;AACjD,CAAC","sourcesContent":["/**\n * Wire contracts of the Pixelume API used by the SDK, with hand-written runtime guards.\n *\n * Source of truth: `pixelume-api` (`docs/openapi.yaml` and `src/interfaces/http/app.ts`).\n * See `docs/api-contract-audit.md`. These types describe JSON on the wire and are internal;\n * public modules map them to SDK types so the API can evolve without breaking consumers.\n *\n * Validation is intentionally tolerant to *additive* changes (unknown fields are ignored,\n * unknown enum values are accepted as strings) and strict about the fields the SDK relies on.\n */\n\nexport type ParseResult<T> =\n { readonly ok: true; readonly value: T } | { readonly ok: false; readonly reason: string };\n\nexport type BodyParser<T> = (body: unknown) => ParseResult<T>;\n\nconst ok = <T>(value: T): ParseResult<T> => ({ ok: true, value });\nconst fail = (reason: string): ParseResult<never> => ({ ok: false, reason });\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** `GET /health` → 200. OpenAPI: `{ status: 'ok', registration: enum, release?: /^[a-f0-9]{7,40}$/ }`. */\nexport interface HealthResponseWire {\n status: 'ok';\n registration: string;\n release?: string;\n}\n\nexport const parseHealthResponse: BodyParser<HealthResponseWire> = (body) => {\n if (!isRecord(body)) return fail('expected a JSON object');\n if (body.status !== 'ok') return fail('expected \"status\" to be \"ok\"');\n if (typeof body.registration !== 'string') return fail('expected \"registration\" to be a string');\n if (body.release !== undefined && typeof body.release !== 'string') {\n return fail('expected \"release\" to be a string when present');\n }\n return ok({\n status: 'ok',\n registration: body.registration,\n ...(body.release === undefined ? {} : { release: body.release }),\n });\n};\n\n/** Error envelope returned by every API error: `{ error: { code, message, requestId } }`. */\nexport interface ErrorEnvelopeWire {\n error: { code: string; message: string; requestId: string };\n}\n\n/** Returns the envelope when `body` matches it, `null` otherwise (e.g. an HTML 502 from a proxy). */\nexport function parseErrorEnvelope(body: unknown): ErrorEnvelopeWire | null {\n if (!isRecord(body) || !isRecord(body.error)) return null;\n const { code, message, requestId } = body.error;\n if (typeof code !== 'string' || typeof message !== 'string' || typeof requestId !== 'string') {\n return null;\n }\n return { error: { code, message, requestId } };\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/contracts/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Wire contracts of the Pixelume API used by the SDK, with hand-written runtime guards.\n *\n * Source of truth: `pixelume-api` (`docs/openapi.yaml` and `src/interfaces/http/app.ts`).\n * See `docs/api-contract-audit.md`. These types describe JSON on the wire and are internal;\n * public modules map them to SDK types so the API can evolve without breaking consumers.\n *\n * Validation is intentionally tolerant to *additive* changes (unknown fields are ignored,\n * unknown enum values are accepted as strings) and strict about the fields the SDK relies on.\n */\n\nexport type ParseResult<T> =\n { readonly ok: true; readonly value: T } | { readonly ok: false; readonly reason: string };\n\nexport type BodyParser<T> = (body: unknown) => ParseResult<T>;\n\nconst ok = <T>(value: T): ParseResult<T> => ({ ok: true, value });\nconst fail = (reason: string): ParseResult<never> => ({ ok: false, reason });\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nexport interface PlayerWire {\n id: string;\n displayName: string;\n avatarUrl: string | null;\n}\n\nexport interface PlayerEnvelopeWire {\n player: PlayerWire;\n}\n\nexport const parsePlayerEnvelope: BodyParser<PlayerEnvelopeWire> = (body) => {\n if (!isRecord(body) || !isRecord(body.player)) {\n return fail('expected a JSON object containing \"player\"');\n }\n const { id, displayName, avatarUrl } = body.player;\n if (typeof id !== 'string' || id.length === 0) return fail('expected \"player.id\" to be a string');\n if (typeof displayName !== 'string') return fail('expected \"player.displayName\" to be a string');\n if (avatarUrl !== null && typeof avatarUrl !== 'string') {\n return fail('expected \"player.avatarUrl\" to be a string or null');\n }\n return ok({ player: { id, displayName, avatarUrl } });\n};\n\nexport interface OAuthTokenResponseWire {\n access_token: string;\n token_type: 'Bearer';\n expires_in: number;\n scope: 'profile';\n}\n\nexport const parseOAuthTokenResponse: BodyParser<OAuthTokenResponseWire> = (body) => {\n if (!isRecord(body)) return fail('expected a JSON object');\n if (\n typeof body.access_token !== 'string' ||\n !/^[A-Za-z0-9_-]{43,128}$/u.test(body.access_token)\n ) {\n return fail('expected \"access_token\" to be a base64url token');\n }\n if (body.token_type !== 'Bearer') return fail('expected \"token_type\" to be \"Bearer\"');\n if (!Number.isInteger(body.expires_in) || (body.expires_in as number) <= 0) {\n return fail('expected \"expires_in\" to be a positive integer');\n }\n if (body.scope !== 'profile') return fail('expected \"scope\" to be \"profile\"');\n return ok({\n access_token: body.access_token,\n token_type: 'Bearer',\n expires_in: body.expires_in as number,\n scope: 'profile',\n });\n};\n\nexport const parseEmptyResponse: BodyParser<void> = (body) =>\n body === undefined ? ok(undefined) : fail('expected an empty response body');\n\n/** `GET /health` → 200. OpenAPI: `{ status: 'ok', registration: enum, release?: /^[a-f0-9]{7,40}$/ }`. */\nexport interface HealthResponseWire {\n status: 'ok';\n registration: string;\n release?: string;\n}\n\nexport const parseHealthResponse: BodyParser<HealthResponseWire> = (body) => {\n if (!isRecord(body)) return fail('expected a JSON object');\n if (body.status !== 'ok') return fail('expected \"status\" to be \"ok\"');\n if (typeof body.registration !== 'string') return fail('expected \"registration\" to be a string');\n if (body.release !== undefined && typeof body.release !== 'string') {\n return fail('expected \"release\" to be a string when present');\n }\n return ok({\n status: 'ok',\n registration: body.registration,\n ...(body.release === undefined ? {} : { release: body.release }),\n });\n};\n\n/** Error envelope returned by every API error: `{ error: { code, message, requestId } }`. */\nexport interface ErrorEnvelopeWire {\n error: { code: string; message: string; requestId: string };\n}\n\n/** Returns the envelope when `body` matches it, `null` otherwise (e.g. an HTML 502 from a proxy). */\nexport function parseErrorEnvelope(body: unknown): ErrorEnvelopeWire | null {\n if (!isRecord(body) || !isRecord(body.error)) return null;\n const { code, message, requestId } = body.error;\n if (typeof code !== 'string' || typeof message !== 'string' || typeof requestId !== 'string') {\n return null;\n }\n return { error: { code, message, requestId } };\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import { type PixelumeAuthApi, type PixelumeAuthState } from '../modules/auth/AuthModule.js';
1
2
  import { type PixelumeHealthApi } from '../modules/health/HealthModule.js';
2
3
  import type { PixelumeClientConfig, PixelumeClientOptions } from './types.js';
3
4
  /**
@@ -14,7 +15,16 @@ import type { PixelumeClientConfig, PixelumeClientOptions } from './types.js';
14
15
  * @throws {PixelumeConfigError} when the options are invalid.
15
16
  */
16
17
  export declare class PixelumeClient {
18
+ #private;
17
19
  readonly config: PixelumeClientConfig;
18
20
  readonly health: PixelumeHealthApi;
21
+ readonly auth: PixelumeAuthApi;
19
22
  constructor(options?: PixelumeClientOptions);
23
+ /**
24
+ * Initializes browser authentication once. Callback failures become an anonymous state so a
25
+ * game can always continue as a visitor; inspect `pixelume.auth.state.error` for diagnostics.
26
+ */
27
+ initialize(): Promise<PixelumeAuthState>;
20
28
  }
29
+ /** Concise alias for new integrations; `PixelumeClient` remains supported. */
30
+ export { PixelumeClient as Pixelume };
@@ -1,4 +1,5 @@
1
1
  import { HttpClient } from '../http/HttpClient.js';
2
+ import { AuthModule, } from '../modules/auth/AuthModule.js';
2
3
  import { HealthModule } from '../modules/health/HealthModule.js';
3
4
  import { resolveConfig } from './config.js';
4
5
  /**
@@ -17,11 +18,17 @@ import { resolveConfig } from './config.js';
17
18
  export class PixelumeClient {
18
19
  config;
19
20
  health;
21
+ auth;
22
+ #authModule;
20
23
  constructor(options = {}) {
21
24
  const resolved = resolveConfig(options);
22
25
  this.config = Object.freeze({
23
26
  apiUrl: resolved.apiUrl,
24
27
  gameId: resolved.gameId,
28
+ clientId: resolved.clientId,
29
+ redirectUri: resolved.redirectUri,
30
+ authUrl: resolved.authUrl,
31
+ restoreSession: resolved.restoreSession,
25
32
  timeoutMs: resolved.timeoutMs,
26
33
  credentials: resolved.credentials,
27
34
  });
@@ -32,6 +39,17 @@ export class PixelumeClient {
32
39
  credentials: resolved.credentials,
33
40
  });
34
41
  this.health = new HealthModule(http);
42
+ this.#authModule = new AuthModule(http, resolved);
43
+ this.auth = this.#authModule;
44
+ }
45
+ /**
46
+ * Initializes browser authentication once. Callback failures become an anonymous state so a
47
+ * game can always continue as a visitor; inspect `pixelume.auth.state.error` for diagnostics.
48
+ */
49
+ initialize() {
50
+ return this.#authModule.initialize();
35
51
  }
36
52
  }
53
+ /** Concise alias for new integrations; `PixelumeClient` remains supported. */
54
+ export { PixelumeClient as Pixelume };
37
55
  //# sourceMappingURL=PixelumeClient.js.map
@@ -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,EAAE,YAAY,EAA0B,MAAM,mCAAmC,CAAC;AACzF,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG5C;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,cAAc;IAChB,MAAM,CAAuB;IAC7B,MAAM,CAAoB;IAEnC,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,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;IACvC,CAAC;CACF","sourcesContent":["import { HttpClient } from '../http/HttpClient.js';\nimport { HealthModule, type PixelumeHealthApi } from '../modules/health/HealthModule.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. Phase 1 ships only `health`; future modules\n * (authentication, saves…) will be added as new properties without changing existing ones.\n *\n * @throws {PixelumeConfigError} when the options are invalid.\n */\nexport class PixelumeClient {\n readonly config: PixelumeClientConfig;\n readonly health: PixelumeHealthApi;\n\n constructor(options: PixelumeClientOptions = {}) {\n const resolved = resolveConfig(options);\n this.config = Object.freeze({\n apiUrl: resolved.apiUrl,\n gameId: resolved.gameId,\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 }\n}\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,aAAa,EAAE,MAAM,aAAa,CAAC;AAG5C;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,cAAc;IAChB,MAAM,CAAuB;IAC7B,MAAM,CAAoB;IAC1B,IAAI,CAAkB;IACtB,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,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;IAC/B,CAAC;IAED;;;OAGG;IACH,UAAU;QACR,OAAO,IAAI,CAAC,WAAW,CAAC,UAAU,EAAE,CAAC;IACvC,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 { 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. Phase 1 ships only `health`; future modules\n * (authentication, saves…) will be added as new properties without changing existing ones.\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 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 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 }\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/** Concise alias for new integrations; `PixelumeClient` remains supported. */\nexport { PixelumeClient as Pixelume };\n"]}
@@ -1,10 +1,17 @@
1
1
  import type { CredentialsPolicy, FetchLike } from '../http/types.js';
2
2
  import type { PixelumeClientOptions } from './types.js';
3
3
  export declare const DEFAULT_API_URL = "https://api.pixelume.com.br";
4
+ export declare const DEFAULT_AUTH_URL = "https://auth.pixelume.com.br";
4
5
  export declare const DEFAULT_TIMEOUT_MS = 10000;
5
6
  export interface ResolvedConfig {
6
7
  apiUrl: string;
7
8
  gameId: string | null;
9
+ clientId: string | null;
10
+ redirectUri: string | null;
11
+ authUrl: string;
12
+ restoreSession: boolean;
13
+ allowInsecureHttp: boolean;
14
+ browser: PixelumeClientOptions['browser'];
8
15
  timeoutMs: number;
9
16
  credentials: CredentialsPolicy;
10
17
  fetch: FetchLike;
@@ -15,11 +22,13 @@ export declare function resolveConfig(options: PixelumeClientOptions): ResolvedC
15
22
  * `[::1]`). Other `http:` hosts (e.g. a LAN IP while testing on a phone) need `allowInsecureHttp`.
16
23
  */
17
24
  export declare function normalizeApiUrl(value: string, allowInsecureHttp: boolean): string;
25
+ export declare function normalizeRedirectUri(value: string, allowInsecureHttp: boolean): string;
18
26
  /**
19
27
  * Game identifiers are public, non-secret slugs (e.g. `rail-puzzle`): lowercase letters, digits
20
28
  * and inner hyphens, 1–64 characters. They identify a game, they never authorize anything.
21
29
  */
22
30
  export declare function validateGameId(value: string): string;
31
+ export declare function validateClientId(value: string): string;
23
32
  /** For feature modules that need a game context (future phases). */
24
33
  export declare function requireGameId(config: Pick<ResolvedConfig, 'gameId'>, feature: string): string;
25
34
  export declare function validateTimeout(value: number): number;
@@ -1,18 +1,48 @@
1
1
  import { PixelumeConfigError } from './errors.js';
2
2
  export const DEFAULT_API_URL = 'https://api.pixelume.com.br';
3
+ export const DEFAULT_AUTH_URL = 'https://auth.pixelume.com.br';
3
4
  export const DEFAULT_TIMEOUT_MS = 10_000;
4
5
  const MAX_TIMEOUT_MS = 2_147_483_647; // setTimeout limit
5
6
  const GAME_ID_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/u;
7
+ const CLIENT_ID_PATTERN = /^[A-Za-z0-9._-]{1,128}$/u;
6
8
  const CREDENTIALS = ['omit', 'same-origin', 'include'];
7
9
  export function resolveConfig(options) {
10
+ const allowInsecureHttp = options.allowInsecureHttp === true;
11
+ const gameId = options.gameId === undefined ? null : validateGameId(options.gameId);
8
12
  return {
9
- apiUrl: normalizeApiUrl(options.apiUrl ?? DEFAULT_API_URL, options.allowInsecureHttp === true),
10
- gameId: options.gameId === undefined ? null : validateGameId(options.gameId),
13
+ apiUrl: normalizeApiUrl(options.apiUrl ?? DEFAULT_API_URL, allowInsecureHttp),
14
+ authUrl: normalizeOrigin(options.authUrl ?? DEFAULT_AUTH_URL, 'authUrl', allowInsecureHttp),
15
+ gameId,
16
+ clientId: options.clientId === undefined ? gameId : validateClientId(options.clientId),
17
+ redirectUri: resolveRedirectUri(options, allowInsecureHttp),
18
+ restoreSession: validateBoolean('restoreSession', options.restoreSession ?? true),
19
+ allowInsecureHttp,
20
+ browser: options.browser,
11
21
  timeoutMs: options.timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : validateTimeout(options.timeoutMs),
12
22
  credentials: validateCredentials(options.credentials ?? 'omit'),
13
23
  fetch: resolveFetch(options.fetch),
14
24
  };
15
25
  }
26
+ function resolveRedirectUri(options, allowInsecureHttp) {
27
+ const value = options.redirectUri ?? defaultRedirectUri(options.browser);
28
+ return value === null ? null : normalizeRedirectUri(value, allowInsecureHttp);
29
+ }
30
+ function validateBoolean(name, value) {
31
+ if (typeof value !== 'boolean')
32
+ throw new PixelumeConfigError(`${name} must be a boolean`);
33
+ return value;
34
+ }
35
+ function defaultRedirectUri(browser) {
36
+ const location = Reflect.get(globalThis, 'location');
37
+ const current = browser?.currentUrl() ??
38
+ (typeof location === 'object' && location !== null && 'href' in location
39
+ ? String(location.href)
40
+ : null);
41
+ if (!current)
42
+ return null;
43
+ const url = new URL(current);
44
+ return `${url.origin}/oauth/callback`;
45
+ }
16
46
  /**
17
47
  * Accepts `https:` URLs, plus `http:` for loopback hosts (`localhost`, `*.localhost`, `127.0.0.0/8`,
18
48
  * `[::1]`). Other `http:` hosts (e.g. a LAN IP while testing on a phone) need `allowInsecureHttp`.
@@ -39,6 +69,38 @@ export function normalizeApiUrl(value, allowInsecureHttp) {
39
69
  }
40
70
  return `${url.origin}${url.pathname.replace(/\/+$/u, '')}`;
41
71
  }
72
+ function normalizeOrigin(value, name, allowInsecureHttp) {
73
+ const normalized = normalizeApiUrl(value, allowInsecureHttp);
74
+ const url = new URL(normalized);
75
+ if (url.pathname !== '/') {
76
+ throw new PixelumeConfigError(`${name} must be an origin without a path`);
77
+ }
78
+ return url.origin;
79
+ }
80
+ export function normalizeRedirectUri(value, allowInsecureHttp) {
81
+ let url;
82
+ try {
83
+ url = new URL(value);
84
+ }
85
+ catch {
86
+ throw new PixelumeConfigError('redirectUri must be an absolute URL registered by the API');
87
+ }
88
+ if (url.protocol !== 'https:' && url.protocol !== 'http:') {
89
+ throw new PixelumeConfigError('redirectUri must use https: (or http: for local development)');
90
+ }
91
+ if (url.protocol === 'http:' && !allowInsecureHttp && !isLoopbackHost(url.hostname)) {
92
+ throw new PixelumeConfigError('redirectUri must use https: outside localhost');
93
+ }
94
+ if (url.username || url.password || url.hash) {
95
+ throw new PixelumeConfigError('redirectUri must not contain credentials or a fragment');
96
+ }
97
+ for (const key of ['code', 'state', 'error', 'error_description']) {
98
+ if (url.searchParams.has(key)) {
99
+ throw new PixelumeConfigError(`redirectUri must not contain the reserved query parameter "${key}"`);
100
+ }
101
+ }
102
+ return url.toString();
103
+ }
42
104
  function isLoopbackHost(hostname) {
43
105
  return (hostname === 'localhost' ||
44
106
  hostname.endsWith('.localhost') ||
@@ -55,6 +117,12 @@ export function validateGameId(value) {
55
117
  }
56
118
  return value;
57
119
  }
120
+ export function validateClientId(value) {
121
+ if (typeof value !== 'string' || !CLIENT_ID_PATTERN.test(value)) {
122
+ throw new PixelumeConfigError('clientId must contain 1-128 letters, digits, dots, underscores or hyphens');
123
+ }
124
+ return value;
125
+ }
58
126
  /** For feature modules that need a game context (future phases). */
59
127
  export function requireGameId(config, feature) {
60
128
  if (config.gameId === null) {