@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 +20 -1
- package/README.md +133 -106
- package/dist/contracts/index.d.ts +17 -0
- package/dist/contracts/index.js +36 -0
- package/dist/contracts/index.js.map +1 -1
- package/dist/core/PixelumeClient.d.ts +10 -0
- package/dist/core/PixelumeClient.js +18 -0
- package/dist/core/PixelumeClient.js.map +1 -1
- package/dist/core/config.d.ts +9 -0
- package/dist/core/config.js +70 -2
- package/dist/core/config.js.map +1 -1
- package/dist/core/errors.d.ts +11 -2
- package/dist/core/errors.js +9 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/types.d.ts +37 -0
- package/dist/core/types.js.map +1 -1
- package/dist/http/HttpClient.js +8 -0
- package/dist/http/HttpClient.js.map +1 -1
- package/dist/http/types.d.ts +2 -0
- package/dist/http/types.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/modules/auth/AuthModule.d.ts +55 -0
- package/dist/modules/auth/AuthModule.js +424 -0
- package/dist/modules/auth/AuthModule.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +2 -2
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.
|
|
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
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
16
|
+
## Configuração obrigatória no backend
|
|
35
17
|
|
|
36
|
-
|
|
37
|
-
import { PixelumeClient } from '@pixelume/sdk';
|
|
18
|
+
Antes de integrar um jogo, registre na Pixelume API:
|
|
38
19
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
28
|
+
## Uso
|
|
49
29
|
|
|
50
|
-
```
|
|
51
|
-
{
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
41
|
+
const initial = await pixelume.initialize();
|
|
42
|
+
// `initialize()` nunca impede o jogo de abrir: falhas de callback viram estado anonymous/error.
|
|
63
43
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
48
|
+
const player = await pixelume.auth.getPlayer(); // PixelumePlayer | null
|
|
71
49
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
63
|
+
// Ao desmontar a tela:
|
|
64
|
+
unsubscribe();
|
|
65
|
+
```
|
|
85
66
|
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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
|
|
125
|
-
npm run
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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).
|
|
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';
|
package/dist/contracts/index.js
CHANGED
|
@@ -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;
|
|
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;
|
|
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"]}
|
package/dist/core/config.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/config.js
CHANGED
|
@@ -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,
|
|
10
|
-
|
|
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) {
|