@pixelume/sdk 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,14 @@
1
1
  import { parseEmptyResponse, parseOAuthTokenResponse, parsePlayerEnvelope, } from '../../contracts/index.js';
2
- import { PixelumeAuthError, PixelumeConfigError, PixelumeHttpError, isPixelumeError, } from '../../core/errors.js';
2
+ import { PixelumeAuthError, PixelumeConfigError, isPixelumeError, } from '../../core/errors.js';
3
+ import { SessionManager, browserResumeSource, } from './SessionManager.js';
3
4
  const AUTHORIZATION_REQUEST_TTL_MS = 10 * 60_000;
4
- const TOKEN_EXPIRY_SKEW_MS = 5_000;
5
5
  const CALLBACK_PARAMETERS = ['code', 'state', 'error', 'error_description'];
6
6
  export class AuthModule {
7
7
  #http;
8
8
  #config;
9
9
  #listeners = new Set();
10
10
  #state = Object.freeze({ status: 'uninitialized', player: null });
11
- #token = null;
11
+ #session;
12
12
  #initializePromise = null;
13
13
  #loginPromise = null;
14
14
  #playerPromise = null;
@@ -16,10 +16,39 @@ export class AuthModule {
16
16
  constructor(http, config) {
17
17
  this.#http = http;
18
18
  this.#config = config;
19
+ const browser = config.browser;
20
+ this.#session = new SessionManager({
21
+ http,
22
+ clientId: () => this.#requireClientId(),
23
+ resume: browser !== undefined && typeof browser.subscribeResume === 'function'
24
+ ? (onResume) => browser.subscribeResume?.(onResume) ?? (() => undefined)
25
+ : browserResumeSource(),
26
+ onEnded: (error) => {
27
+ this.#setState(anonymous('expired', error));
28
+ },
29
+ });
19
30
  }
20
31
  get state() {
21
32
  return this.#state;
22
33
  }
34
+ get session() {
35
+ return this.#session.state;
36
+ }
37
+ /** @internal Shared with feature modules that call Bearer-protected routes. */
38
+ get sessionManager() {
39
+ return this.#session;
40
+ }
41
+ subscribeSession(listener) {
42
+ return this.#session.subscribe(listener);
43
+ }
44
+ refresh(options = {}) {
45
+ return this.#session.refresh(options);
46
+ }
47
+ /** @internal Releases timers/listeners and drops credentials from memory (no revocation). */
48
+ dispose() {
49
+ this.#session.dispose();
50
+ this.#listeners.clear();
51
+ }
23
52
  initialize() {
24
53
  this.#initializePromise ??= this.#doInitialize();
25
54
  return this.#initializePromise;
@@ -29,23 +58,18 @@ export class AuthModule {
29
58
  return () => this.#listeners.delete(listener);
30
59
  }
31
60
  login(options = {}) {
32
- const scope = options.scope;
33
- if (scope !== undefined && scope !== 'profile') {
34
- return Promise.reject(new PixelumeConfigError('Pixelume Auth supports only the "profile" scope'));
61
+ const scope = options.scope ?? this.#config.scope;
62
+ if (scope !== 'profile' && scope !== 'profile saves') {
63
+ return Promise.reject(new PixelumeConfigError('Pixelume Auth scope must be "profile" or "profile saves"'));
35
64
  }
36
- this.#loginPromise ??= this.#doLogin().finally(() => {
65
+ this.#loginPromise ??= this.#doLogin(scope).finally(() => {
37
66
  this.#loginPromise = null;
38
67
  });
39
68
  return this.#loginPromise;
40
69
  }
41
70
  getPlayer(options = {}) {
42
- if (this.#token === null)
43
- return Promise.resolve(null);
44
- if (this.#token.expiresAt <= Date.now() + TOKEN_EXPIRY_SKEW_MS) {
45
- this.#token = null;
46
- this.#setState(anonymous('expired'));
71
+ if (!this.#session.active)
47
72
  return Promise.resolve(null);
48
- }
49
73
  this.#playerPromise ??= this.#fetchPlayer(options).finally(() => {
50
74
  this.#playerPromise = null;
51
75
  });
@@ -64,9 +88,15 @@ export class AuthModule {
64
88
  if (callback !== null) {
65
89
  await this.#handleCallback(browser, callback);
66
90
  }
67
- else if (this.#config.restoreSession && this.#readSessionHint(browser.sessionStorage)) {
68
- this.#removeSessionHint(browser.sessionStorage);
69
- await this.login();
91
+ else if (this.#config.restoreSession) {
92
+ const scope = this.#readSessionHint(browser.sessionStorage);
93
+ if (scope === null) {
94
+ this.#setState(anonymous('visitor'));
95
+ }
96
+ else {
97
+ this.#removeSessionHint(browser.sessionStorage);
98
+ await this.login({ scope });
99
+ }
70
100
  }
71
101
  else {
72
102
  this.#setState(anonymous('visitor'));
@@ -80,7 +110,7 @@ export class AuthModule {
80
110
  }
81
111
  return this.#state;
82
112
  }
83
- async #doLogin() {
113
+ async #doLogin(scope) {
84
114
  const clientId = this.#requireClientId();
85
115
  const redirectUri = this.#requireRedirectUri();
86
116
  const browser = this.#browser();
@@ -104,7 +134,7 @@ export class AuthModule {
104
134
  state: pending.state,
105
135
  code_challenge: challenge,
106
136
  code_challenge_method: 'S256',
107
- scope: 'profile',
137
+ scope,
108
138
  }).toString();
109
139
  this.#setState(Object.freeze({ status: 'redirecting', player: null }));
110
140
  try {
@@ -158,44 +188,40 @@ export class AuthModule {
158
188
  },
159
189
  parse: parseOAuthTokenResponse,
160
190
  });
161
- this.#token = {
162
- value: response.data.access_token,
163
- expiresAt: Date.now() + response.data.expires_in * 1_000,
164
- };
191
+ this.#session.start(response.data);
165
192
  const player = await this.getPlayer();
166
- if (player !== null)
167
- this.#writeSessionHint(browser.sessionStorage);
193
+ if (player !== null) {
194
+ const scope = response.data.scopes.includes('saves') ? 'profile saves' : 'profile';
195
+ this.#writeSessionHint(browser.sessionStorage, scope);
196
+ }
168
197
  }
169
198
  async #fetchPlayer(options) {
170
- const token = this.#token;
171
- if (token === null)
172
- return null;
173
199
  try {
174
- const response = await this.#http.request({
200
+ const response = await this.#session.authorizedRequest({
175
201
  method: 'GET',
176
202
  path: '/v1/oauth/userinfo',
177
- credentials: 'omit',
178
- headers: { authorization: `Bearer ${token.value}` },
179
203
  signal: options.signal,
180
204
  timeoutMs: options.timeoutMs,
181
205
  parse: parsePlayerEnvelope,
182
206
  });
183
207
  const player = Object.freeze({ ...response.data.player });
208
+ const current = this.#state;
209
+ // Keep the same object when nothing changed, so renewals do not look like a new login.
210
+ if (current.status === 'authenticated' && samePlayer(current.player, player)) {
211
+ return current.player;
212
+ }
184
213
  this.#setState(Object.freeze({ status: 'authenticated', player }));
185
214
  return player;
186
215
  }
187
216
  catch (error) {
188
- if (error instanceof PixelumeHttpError && error.status === 401) {
189
- this.#token = null;
190
- this.#setState(anonymous('expired'));
217
+ // The session ended (expired/revoked); the state already reflects it.
218
+ if (error instanceof PixelumeAuthError)
191
219
  return null;
192
- }
193
220
  throw error;
194
221
  }
195
222
  }
196
223
  async #doLogout(options) {
197
- const token = this.#token;
198
- this.#token = null;
224
+ const token = this.#session.reset();
199
225
  const browser = this.#optionalBrowser();
200
226
  if (browser) {
201
227
  this.#removePending(browser.sessionStorage);
@@ -204,11 +230,16 @@ export class AuthModule {
204
230
  this.#setState(anonymous('logged_out'));
205
231
  if (token === null)
206
232
  return;
233
+ // Revoking any token of the family revokes all of it; the refresh token is the preferred hint.
207
234
  await this.#http.request({
208
235
  method: 'POST',
209
236
  path: '/v1/oauth/revoke',
210
237
  credentials: 'omit',
211
- form: { token: token.value, token_type_hint: 'access_token' },
238
+ form: {
239
+ token: token.refreshToken ?? token.accessToken,
240
+ token_type_hint: token.refreshToken === null ? 'access_token' : 'refresh_token',
241
+ client_id: this.#requireClientId(),
242
+ },
212
243
  signal: options.signal,
213
244
  timeoutMs: options.timeoutMs,
214
245
  parse: parseEmptyResponse,
@@ -326,17 +357,23 @@ export class AuthModule {
326
357
  throw new PixelumeAuthError('storage_unavailable', { cause });
327
358
  }
328
359
  }
360
+ /** The hint is non-secret: only "a session existed" and the scope that was granted. */
329
361
  #readSessionHint(storage) {
330
362
  try {
331
- return storage.getItem(this.#hintKey()) === '1';
363
+ const value = storage.getItem(this.#hintKey());
364
+ if (value === '1' || value === 'profile')
365
+ return 'profile';
366
+ if (value === 'profile saves')
367
+ return 'profile saves';
368
+ return null;
332
369
  }
333
370
  catch (cause) {
334
371
  throw new PixelumeAuthError('storage_unavailable', { cause });
335
372
  }
336
373
  }
337
- #writeSessionHint(storage) {
374
+ #writeSessionHint(storage, scope) {
338
375
  try {
339
- storage.setItem(this.#hintKey(), '1');
376
+ storage.setItem(this.#hintKey(), scope);
340
377
  }
341
378
  catch (cause) {
342
379
  throw new PixelumeAuthError('storage_unavailable', { cause });
@@ -366,6 +403,11 @@ export class AuthModule {
366
403
  function anonymous(reason, error = null) {
367
404
  return Object.freeze({ status: 'anonymous', player: null, reason, error });
368
405
  }
406
+ function samePlayer(left, right) {
407
+ return (left.id === right.id &&
408
+ left.displayName === right.displayName &&
409
+ left.avatarUrl === right.avatarUrl);
410
+ }
369
411
  function isPendingAuthorization(value) {
370
412
  if (typeof value !== 'object' || value === null || Array.isArray(value))
371
413
  return false;
@@ -1 +1 @@
1
- {"version":3,"file":"AuthModule.js","sourceRoot":"","sources":["../../../src/modules/auth/AuthModule.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n parseEmptyResponse,\n parseOAuthTokenResponse,\n parsePlayerEnvelope,\n} from '../../contracts/index.js';\nimport {\n PixelumeAuthError,\n PixelumeConfigError,\n PixelumeHttpError,\n isPixelumeError,\n type PixelumeError,\n} from '../../core/errors.js';\nimport type {\n PixelumeBrowserAdapter,\n PixelumeRequestOptions,\n PixelumeSessionStorage,\n} from '../../core/types.js';\nimport type { ResolvedConfig } from '../../core/config.js';\nimport type { HttpClient } from '../../http/HttpClient.js';\n\nconst AUTHORIZATION_REQUEST_TTL_MS = 10 * 60_000;\nconst TOKEN_EXPIRY_SKEW_MS = 5_000;\nconst CALLBACK_PARAMETERS = ['code', 'state', 'error', 'error_description'] as const;\n\nexport interface PixelumePlayer {\n readonly id: string;\n readonly displayName: string;\n readonly avatarUrl: string | null;\n}\n\nexport type PixelumeAnonymousReason = 'visitor' | 'cancelled' | 'expired' | 'logged_out' | 'error';\n\nexport type PixelumeAuthState =\n | { readonly status: 'uninitialized'; readonly player: null }\n | { readonly status: 'redirecting'; readonly player: null }\n | { readonly status: 'exchanging'; readonly player: null }\n | {\n readonly status: 'anonymous';\n readonly player: null;\n readonly reason: PixelumeAnonymousReason;\n readonly error: PixelumeError | null;\n }\n | { readonly status: 'authenticated'; readonly player: PixelumePlayer };\n\nexport type PixelumeAuthListener = (state: PixelumeAuthState, previous: PixelumeAuthState) => void;\n\nexport interface PixelumeLoginOptions {\n /** OAuth scope. The current API accepts only `profile`. */\n readonly scope?: 'profile';\n}\n\nexport interface PixelumeAuthApi {\n /** Current synchronous authentication state. */\n readonly state: PixelumeAuthState;\n /** Starts a top-level redirect to the API authorization endpoint. */\n login(options?: PixelumeLoginOptions): Promise<void>;\n /** Returns the current player, revalidating the memory-only access token with the API. */\n getPlayer(options?: PixelumeRequestOptions): Promise<PixelumePlayer | null>;\n /** Revokes this game's access token and clears local authentication state. */\n logout(options?: PixelumeRequestOptions): Promise<void>;\n /** Subscribes to state changes. The returned function removes the listener. */\n subscribe(listener: PixelumeAuthListener): () => void;\n}\n\ninterface PendingAuthorization {\n readonly version: 1;\n readonly state: string;\n readonly verifier: string;\n readonly redirectUri: string;\n readonly createdAt: number;\n}\n\ninterface AccessToken {\n readonly value: string;\n readonly expiresAt: number;\n}\n\nexport class AuthModule implements PixelumeAuthApi {\n readonly #http: HttpClient;\n readonly #config: ResolvedConfig;\n readonly #listeners = new Set<PixelumeAuthListener>();\n #state: PixelumeAuthState = Object.freeze({ status: 'uninitialized', player: null });\n #token: AccessToken | null = null;\n #initializePromise: Promise<PixelumeAuthState> | null = null;\n #loginPromise: Promise<void> | null = null;\n #playerPromise: Promise<PixelumePlayer | null> | null = null;\n #logoutPromise: Promise<void> | null = null;\n\n constructor(http: HttpClient, config: ResolvedConfig) {\n this.#http = http;\n this.#config = config;\n }\n\n get state(): PixelumeAuthState {\n return this.#state;\n }\n\n initialize(): Promise<PixelumeAuthState> {\n this.#initializePromise ??= this.#doInitialize();\n return this.#initializePromise;\n }\n\n subscribe(listener: PixelumeAuthListener): () => void {\n this.#listeners.add(listener);\n return () => this.#listeners.delete(listener);\n }\n\n login(options: PixelumeLoginOptions = {}): Promise<void> {\n const scope: unknown = options.scope;\n if (scope !== undefined && scope !== 'profile') {\n return Promise.reject(\n new PixelumeConfigError('Pixelume Auth supports only the \"profile\" scope'),\n );\n }\n this.#loginPromise ??= this.#doLogin().finally(() => {\n this.#loginPromise = null;\n });\n return this.#loginPromise;\n }\n\n getPlayer(options: PixelumeRequestOptions = {}): Promise<PixelumePlayer | null> {\n if (this.#token === null) return Promise.resolve(null);\n if (this.#token.expiresAt <= Date.now() + TOKEN_EXPIRY_SKEW_MS) {\n this.#token = null;\n this.#setState(anonymous('expired'));\n return Promise.resolve(null);\n }\n this.#playerPromise ??= this.#fetchPlayer(options).finally(() => {\n this.#playerPromise = null;\n });\n return this.#playerPromise;\n }\n\n logout(options: PixelumeRequestOptions = {}): Promise<void> {\n this.#logoutPromise ??= this.#doLogout(options).finally(() => {\n this.#logoutPromise = null;\n });\n return this.#logoutPromise;\n }\n\n async #doInitialize(): Promise<PixelumeAuthState> {\n try {\n const browser = this.#browser();\n const callback = this.#readCallback(browser.currentUrl());\n if (callback !== null) {\n await this.#handleCallback(browser, callback);\n } else if (this.#config.restoreSession && this.#readSessionHint(browser.sessionStorage)) {\n this.#removeSessionHint(browser.sessionStorage);\n await this.login();\n } else {\n this.#setState(anonymous('visitor'));\n }\n } catch (error) {\n const failure = isPixelumeError(error)\n ? error\n : new PixelumeAuthError('browser_unavailable', { cause: error });\n this.#setState(anonymous('error', failure));\n }\n return this.#state;\n }\n\n async #doLogin(): Promise<void> {\n const clientId = this.#requireClientId();\n const redirectUri = this.#requireRedirectUri();\n const browser = this.#browser();\n let pending = this.#readPending(browser.sessionStorage);\n if (pending === null || pending.redirectUri !== redirectUri || this.#isExpired(pending)) {\n pending = {\n version: 1,\n state: randomBase64Url(browser.crypto),\n verifier: randomBase64Url(browser.crypto),\n redirectUri,\n createdAt: Date.now(),\n };\n this.#writePending(browser.sessionStorage, pending);\n }\n const challenge = await sha256Base64Url(browser.crypto, pending.verifier);\n const authorization = new URL('/v1/oauth/authorize', this.#config.apiUrl);\n authorization.search = new URLSearchParams({\n client_id: clientId,\n redirect_uri: redirectUri,\n response_type: 'code',\n state: pending.state,\n code_challenge: challenge,\n code_challenge_method: 'S256',\n scope: 'profile',\n }).toString();\n this.#setState(Object.freeze({ status: 'redirecting', player: null }));\n try {\n browser.navigate(authorization.toString());\n } catch (cause) {\n this.#removePending(browser.sessionStorage);\n throw new PixelumeAuthError('browser_unavailable', { cause });\n }\n }\n\n async #handleCallback(\n browser: PixelumeBrowserAdapter,\n callback: {\n readonly url: URL;\n readonly code: string | null;\n readonly error: string | null;\n readonly invalid: boolean;\n },\n ): Promise<void> {\n const pending = this.#readPending(browser.sessionStorage);\n this.#removePending(browser.sessionStorage);\n this.#cleanCallback(browser, callback.url);\n\n if (callback.invalid) throw new PixelumeAuthError('invalid_callback');\n if (pending === null || this.#isExpired(pending)) {\n this.#removeSessionHint(browser.sessionStorage);\n throw new PixelumeAuthError(pending === null ? 'invalid_callback' : 'authorization_expired');\n }\n if (\n pending.redirectUri !== this.#config.redirectUri ||\n (callback.code !== null && !/^[A-Za-z0-9_-]{43,128}$/u.test(callback.code))\n ) {\n throw new PixelumeAuthError('invalid_callback');\n }\n const returnedState = singleParameter(callback.url, 'state');\n if (returnedState === null || returnedState !== pending.state) {\n this.#removeSessionHint(browser.sessionStorage);\n throw new PixelumeAuthError('state_mismatch');\n }\n if (callback.error !== null) {\n this.#removeSessionHint(browser.sessionStorage);\n if (callback.error === 'access_denied') {\n this.#setState(anonymous('cancelled'));\n return;\n }\n throw new PixelumeAuthError('invalid_callback');\n }\n if (callback.code === null) throw new PixelumeAuthError('invalid_callback');\n\n this.#setState(Object.freeze({ status: 'exchanging', player: null }));\n const response = await this.#http.request({\n method: 'POST',\n path: '/v1/oauth/token',\n credentials: 'omit',\n form: {\n grant_type: 'authorization_code',\n code: callback.code,\n client_id: this.#requireClientId(),\n redirect_uri: pending.redirectUri,\n code_verifier: pending.verifier,\n },\n parse: parseOAuthTokenResponse,\n });\n this.#token = {\n value: response.data.access_token,\n expiresAt: Date.now() + response.data.expires_in * 1_000,\n };\n const player = await this.getPlayer();\n if (player !== null) this.#writeSessionHint(browser.sessionStorage);\n }\n\n async #fetchPlayer(options: PixelumeRequestOptions): Promise<PixelumePlayer | null> {\n const token = this.#token;\n if (token === null) return null;\n try {\n const response = await this.#http.request({\n method: 'GET',\n path: '/v1/oauth/userinfo',\n credentials: 'omit',\n headers: { authorization: `Bearer ${token.value}` },\n signal: options.signal,\n timeoutMs: options.timeoutMs,\n parse: parsePlayerEnvelope,\n });\n const player = Object.freeze({ ...response.data.player });\n this.#setState(Object.freeze({ status: 'authenticated', player }));\n return player;\n } catch (error) {\n if (error instanceof PixelumeHttpError && error.status === 401) {\n this.#token = null;\n this.#setState(anonymous('expired'));\n return null;\n }\n throw error;\n }\n }\n\n async #doLogout(options: PixelumeRequestOptions): Promise<void> {\n const token = this.#token;\n this.#token = null;\n const browser = this.#optionalBrowser();\n if (browser) {\n this.#removePending(browser.sessionStorage);\n this.#removeSessionHint(browser.sessionStorage);\n }\n this.#setState(anonymous('logged_out'));\n if (token === null) return;\n await this.#http.request({\n method: 'POST',\n path: '/v1/oauth/revoke',\n credentials: 'omit',\n form: { token: token.value, token_type_hint: 'access_token' },\n signal: options.signal,\n timeoutMs: options.timeoutMs,\n parse: parseEmptyResponse,\n });\n }\n\n #readCallback(value: string): {\n readonly url: URL;\n readonly code: string | null;\n readonly error: string | null;\n readonly invalid: boolean;\n } | null {\n const redirectUri = this.#config.redirectUri;\n if (redirectUri === null) return null;\n const current = new URL(value);\n const expected = new URL(redirectUri);\n if (current.origin !== expected.origin || current.pathname !== expected.pathname) return null;\n const hasCallbackParameter = CALLBACK_PARAMETERS.some((key) => current.searchParams.has(key));\n if (!hasCallbackParameter) return null;\n const code = singleParameter(current, 'code');\n const error = singleParameter(current, 'error');\n return {\n url: current,\n code,\n error,\n invalid: !sameCallbackBase(current, expected) || (code === null) === (error === null),\n };\n }\n\n #cleanCallback(browser: PixelumeBrowserAdapter, url: URL): void {\n const clean = new URL(url.toString());\n for (const key of CALLBACK_PARAMETERS) clean.searchParams.delete(key);\n browser.replaceUrl(`${clean.pathname}${clean.search}${clean.hash}`);\n }\n\n #browser(): PixelumeBrowserAdapter {\n const browser = this.#optionalBrowser();\n if (!browser) throw new PixelumeAuthError('browser_unavailable');\n if (\n typeof browser.crypto.getRandomValues !== 'function' ||\n typeof browser.crypto.subtle.digest !== 'function'\n ) {\n throw new PixelumeAuthError('crypto_unavailable');\n }\n return browser;\n }\n\n #optionalBrowser(): PixelumeBrowserAdapter | null {\n if (this.#config.browser) return this.#config.browser;\n try {\n if (\n typeof globalThis.location === 'undefined' ||\n typeof globalThis.history === 'undefined' ||\n typeof globalThis.sessionStorage === 'undefined' ||\n typeof globalThis.crypto === 'undefined'\n ) {\n return null;\n }\n return {\n currentUrl: () => globalThis.location.href,\n navigate: (url) => {\n globalThis.location.assign(url);\n },\n replaceUrl: (url) => {\n globalThis.history.replaceState(globalThis.history.state, '', url);\n },\n sessionStorage: globalThis.sessionStorage,\n crypto: globalThis.crypto,\n };\n } catch (cause) {\n throw new PixelumeAuthError('storage_unavailable', { cause });\n }\n }\n\n #requireClientId(): string {\n if (this.#config.clientId === null) {\n throw new PixelumeConfigError('Pixelume Auth requires clientId or gameId');\n }\n return this.#config.clientId;\n }\n\n #requireRedirectUri(): string {\n if (this.#config.redirectUri === null) {\n throw new PixelumeAuthError('browser_unavailable');\n }\n return this.#config.redirectUri;\n }\n\n #pendingKey(): string {\n return `pixelume.oauth.pending.${this.#config.clientId ?? 'unconfigured'}`;\n }\n\n #hintKey(): string {\n return `pixelume.oauth.session.${this.#config.clientId ?? 'unconfigured'}`;\n }\n\n #readPending(storage: PixelumeSessionStorage): PendingAuthorization | null {\n try {\n const raw = storage.getItem(this.#pendingKey());\n if (raw === null) return null;\n const value: unknown = JSON.parse(raw);\n if (!isPendingAuthorization(value)) {\n storage.removeItem(this.#pendingKey());\n return null;\n }\n return value;\n } catch (cause) {\n throw new PixelumeAuthError('storage_unavailable', { cause });\n }\n }\n\n #writePending(storage: PixelumeSessionStorage, pending: PendingAuthorization): void {\n try {\n storage.setItem(this.#pendingKey(), JSON.stringify(pending));\n } catch (cause) {\n throw new PixelumeAuthError('storage_unavailable', { cause });\n }\n }\n\n #removePending(storage: PixelumeSessionStorage): void {\n try {\n storage.removeItem(this.#pendingKey());\n } catch (cause) {\n throw new PixelumeAuthError('storage_unavailable', { cause });\n }\n }\n\n #readSessionHint(storage: PixelumeSessionStorage): boolean {\n try {\n return storage.getItem(this.#hintKey()) === '1';\n } catch (cause) {\n throw new PixelumeAuthError('storage_unavailable', { cause });\n }\n }\n\n #writeSessionHint(storage: PixelumeSessionStorage): void {\n try {\n storage.setItem(this.#hintKey(), '1');\n } catch (cause) {\n throw new PixelumeAuthError('storage_unavailable', { cause });\n }\n }\n\n #removeSessionHint(storage: PixelumeSessionStorage): void {\n try {\n storage.removeItem(this.#hintKey());\n } catch (cause) {\n throw new PixelumeAuthError('storage_unavailable', { cause });\n }\n }\n\n #isExpired(pending: PendingAuthorization): boolean {\n const now = Date.now();\n return (\n pending.createdAt > now + 60_000 || pending.createdAt + AUTHORIZATION_REQUEST_TTL_MS < now\n );\n }\n\n #setState(state: PixelumeAuthState): void {\n const previous = this.#state;\n if (sameState(previous, state)) return;\n this.#state = state;\n for (const listener of [...this.#listeners]) listener(state, previous);\n }\n}\n\nfunction anonymous(\n reason: PixelumeAnonymousReason,\n error: PixelumeError | null = null,\n): PixelumeAuthState {\n return Object.freeze({ status: 'anonymous', player: null, reason, error });\n}\n\nfunction isPendingAuthorization(value: unknown): value is PendingAuthorization {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;\n const pending = value as Record<string, unknown>;\n return (\n pending.version === 1 &&\n typeof pending.state === 'string' &&\n /^[A-Za-z0-9_-]{43}$/u.test(pending.state) &&\n typeof pending.verifier === 'string' &&\n /^[A-Za-z0-9_-]{43}$/u.test(pending.verifier) &&\n typeof pending.redirectUri === 'string' &&\n typeof pending.createdAt === 'number' &&\n Number.isFinite(pending.createdAt)\n );\n}\n\nfunction sameCallbackBase(current: URL, expected: URL): boolean {\n if (current.origin !== expected.origin || current.pathname !== expected.pathname) return false;\n const currentBase = new URL(current.toString());\n for (const key of CALLBACK_PARAMETERS) currentBase.searchParams.delete(key);\n currentBase.hash = '';\n const expectedBase = new URL(expected.toString());\n expectedBase.hash = '';\n return currentBase.toString() === expectedBase.toString();\n}\n\nfunction singleParameter(url: URL, name: string): string | null {\n const values = url.searchParams.getAll(name);\n if (values.length !== 1 || values[0] === '') return null;\n return values[0] ?? null;\n}\n\nfunction randomBase64Url(crypto: Crypto): string {\n const bytes = crypto.getRandomValues(new Uint8Array(32));\n return bytesToBase64Url(bytes);\n}\n\nasync function sha256Base64Url(crypto: Crypto, value: string): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value));\n return bytesToBase64Url(new Uint8Array(digest));\n}\n\nfunction bytesToBase64Url(bytes: Uint8Array): string {\n let binary = '';\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/u, '');\n}\n\nfunction sameState(left: PixelumeAuthState, right: PixelumeAuthState): boolean {\n if (left.status !== right.status) return false;\n if (left.status === 'authenticated' && right.status === 'authenticated') {\n return left.player === right.player;\n }\n if (left.status === 'anonymous' && right.status === 'anonymous') {\n return left.reason === right.reason && left.error === right.error;\n }\n return true;\n}\n"]}
1
+ {"version":3,"file":"AuthModule.js","sourceRoot":"","sources":["../../../src/modules/auth/AuthModule.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n parseEmptyResponse,\n parseOAuthTokenResponse,\n parsePlayerEnvelope,\n} from '../../contracts/index.js';\nimport {\n PixelumeAuthError,\n PixelumeConfigError,\n isPixelumeError,\n type PixelumeError,\n} from '../../core/errors.js';\nimport type {\n PixelumeBrowserAdapter,\n PixelumeRequestOptions,\n PixelumeSessionStorage,\n} from '../../core/types.js';\nimport type { ResolvedConfig } from '../../core/config.js';\nimport type { HttpClient } from '../../http/HttpClient.js';\nimport {\n SessionManager,\n browserResumeSource,\n type PixelumeSessionListener,\n type PixelumeSessionState,\n} from './SessionManager.js';\n\nconst AUTHORIZATION_REQUEST_TTL_MS = 10 * 60_000;\nconst CALLBACK_PARAMETERS = ['code', 'state', 'error', 'error_description'] as const;\n\nexport interface PixelumePlayer {\n readonly id: string;\n readonly displayName: string;\n readonly avatarUrl: string | null;\n}\n\nexport type PixelumeAnonymousReason = 'visitor' | 'cancelled' | 'expired' | 'logged_out' | 'error';\n\nexport type PixelumeAuthState =\n | { readonly status: 'uninitialized'; readonly player: null }\n | { readonly status: 'redirecting'; readonly player: null }\n | { readonly status: 'exchanging'; readonly player: null }\n | {\n readonly status: 'anonymous';\n readonly player: null;\n readonly reason: PixelumeAnonymousReason;\n readonly error: PixelumeError | null;\n }\n | { readonly status: 'authenticated'; readonly player: PixelumePlayer };\n\nexport type PixelumeAuthListener = (state: PixelumeAuthState, previous: PixelumeAuthState) => void;\n\n/**\n * OAuth scope requested at login. `profile` identifies the player; `profile saves` also allows\n * Cloud Saves and requires `saves` to be enabled for the game in the Pixelume Game Registry.\n */\nexport type PixelumeScopeRequest = 'profile' | 'profile saves';\n\nexport interface PixelumeLoginOptions {\n /** Defaults to the client `scope` option (`profile` unless configured). */\n readonly scope?: PixelumeScopeRequest;\n}\n\nexport interface PixelumeAuthApi {\n /** Current synchronous authentication state. */\n readonly state: PixelumeAuthState;\n /** Starts a top-level redirect to the API authorization endpoint. */\n login(options?: PixelumeLoginOptions): Promise<void>;\n /** Returns the current player, revalidating the memory-only access token with the API. */\n getPlayer(options?: PixelumeRequestOptions): Promise<PixelumePlayer | null>;\n /**\n * Revokes this game's authorization (refresh token family and access tokens) and clears local\n * authentication state. The central Pixelume session in the portal is not affected.\n */\n logout(options?: PixelumeRequestOptions): Promise<void>;\n /** Subscribes to state changes. The returned function removes the listener. */\n subscribe(listener: PixelumeAuthListener): () => void;\n /**\n * Token lifecycle of the signed-in player (scopes, expiry, renewal in flight, last transient\n * renewal error). Never exposes tokens.\n */\n readonly session: PixelumeSessionState;\n /** Subscribes to `session` changes. The returned function removes the listener. */\n subscribeSession(listener: PixelumeSessionListener): () => void;\n /**\n * Renews the access token now. Renewal is automatic; call this only to renew ahead of a\n * critical operation. Rejects with the transient error (the session is kept and retried) or\n * with `PixelumeAuthError` when there is no session or it ended.\n */\n refresh(options?: PixelumeRequestOptions): Promise<void>;\n}\n\ninterface PendingAuthorization {\n readonly version: 1;\n readonly state: string;\n readonly verifier: string;\n readonly redirectUri: string;\n readonly createdAt: number;\n}\n\nexport class AuthModule implements PixelumeAuthApi {\n readonly #http: HttpClient;\n readonly #config: ResolvedConfig;\n readonly #listeners = new Set<PixelumeAuthListener>();\n #state: PixelumeAuthState = Object.freeze({ status: 'uninitialized', player: null });\n readonly #session: SessionManager;\n #initializePromise: Promise<PixelumeAuthState> | null = null;\n #loginPromise: Promise<void> | null = null;\n #playerPromise: Promise<PixelumePlayer | null> | null = null;\n #logoutPromise: Promise<void> | null = null;\n\n constructor(http: HttpClient, config: ResolvedConfig) {\n this.#http = http;\n this.#config = config;\n const browser = config.browser;\n this.#session = new SessionManager({\n http,\n clientId: () => this.#requireClientId(),\n resume:\n browser !== undefined && typeof browser.subscribeResume === 'function'\n ? (onResume) => browser.subscribeResume?.(onResume) ?? (() => undefined)\n : browserResumeSource(),\n onEnded: (error) => {\n this.#setState(anonymous('expired', error));\n },\n });\n }\n\n get state(): PixelumeAuthState {\n return this.#state;\n }\n\n get session(): PixelumeSessionState {\n return this.#session.state;\n }\n\n /** @internal Shared with feature modules that call Bearer-protected routes. */\n get sessionManager(): SessionManager {\n return this.#session;\n }\n\n subscribeSession(listener: PixelumeSessionListener): () => void {\n return this.#session.subscribe(listener);\n }\n\n refresh(options: PixelumeRequestOptions = {}): Promise<void> {\n return this.#session.refresh(options);\n }\n\n /** @internal Releases timers/listeners and drops credentials from memory (no revocation). */\n dispose(): void {\n this.#session.dispose();\n this.#listeners.clear();\n }\n\n initialize(): Promise<PixelumeAuthState> {\n this.#initializePromise ??= this.#doInitialize();\n return this.#initializePromise;\n }\n\n subscribe(listener: PixelumeAuthListener): () => void {\n this.#listeners.add(listener);\n return () => this.#listeners.delete(listener);\n }\n\n login(options: PixelumeLoginOptions = {}): Promise<void> {\n const scope: unknown = options.scope ?? this.#config.scope;\n if (scope !== 'profile' && scope !== 'profile saves') {\n return Promise.reject(\n new PixelumeConfigError('Pixelume Auth scope must be \"profile\" or \"profile saves\"'),\n );\n }\n this.#loginPromise ??= this.#doLogin(scope).finally(() => {\n this.#loginPromise = null;\n });\n return this.#loginPromise;\n }\n\n getPlayer(options: PixelumeRequestOptions = {}): Promise<PixelumePlayer | null> {\n if (!this.#session.active) return Promise.resolve(null);\n this.#playerPromise ??= this.#fetchPlayer(options).finally(() => {\n this.#playerPromise = null;\n });\n return this.#playerPromise;\n }\n\n logout(options: PixelumeRequestOptions = {}): Promise<void> {\n this.#logoutPromise ??= this.#doLogout(options).finally(() => {\n this.#logoutPromise = null;\n });\n return this.#logoutPromise;\n }\n\n async #doInitialize(): Promise<PixelumeAuthState> {\n try {\n const browser = this.#browser();\n const callback = this.#readCallback(browser.currentUrl());\n if (callback !== null) {\n await this.#handleCallback(browser, callback);\n } else if (this.#config.restoreSession) {\n const scope = this.#readSessionHint(browser.sessionStorage);\n if (scope === null) {\n this.#setState(anonymous('visitor'));\n } else {\n this.#removeSessionHint(browser.sessionStorage);\n await this.login({ scope });\n }\n } else {\n this.#setState(anonymous('visitor'));\n }\n } catch (error) {\n const failure = isPixelumeError(error)\n ? error\n : new PixelumeAuthError('browser_unavailable', { cause: error });\n this.#setState(anonymous('error', failure));\n }\n return this.#state;\n }\n\n async #doLogin(scope: PixelumeScopeRequest): Promise<void> {\n const clientId = this.#requireClientId();\n const redirectUri = this.#requireRedirectUri();\n const browser = this.#browser();\n let pending = this.#readPending(browser.sessionStorage);\n if (pending === null || pending.redirectUri !== redirectUri || this.#isExpired(pending)) {\n pending = {\n version: 1,\n state: randomBase64Url(browser.crypto),\n verifier: randomBase64Url(browser.crypto),\n redirectUri,\n createdAt: Date.now(),\n };\n this.#writePending(browser.sessionStorage, pending);\n }\n const challenge = await sha256Base64Url(browser.crypto, pending.verifier);\n const authorization = new URL('/v1/oauth/authorize', this.#config.apiUrl);\n authorization.search = new URLSearchParams({\n client_id: clientId,\n redirect_uri: redirectUri,\n response_type: 'code',\n state: pending.state,\n code_challenge: challenge,\n code_challenge_method: 'S256',\n scope,\n }).toString();\n this.#setState(Object.freeze({ status: 'redirecting', player: null }));\n try {\n browser.navigate(authorization.toString());\n } catch (cause) {\n this.#removePending(browser.sessionStorage);\n throw new PixelumeAuthError('browser_unavailable', { cause });\n }\n }\n\n async #handleCallback(\n browser: PixelumeBrowserAdapter,\n callback: {\n readonly url: URL;\n readonly code: string | null;\n readonly error: string | null;\n readonly invalid: boolean;\n },\n ): Promise<void> {\n const pending = this.#readPending(browser.sessionStorage);\n this.#removePending(browser.sessionStorage);\n this.#cleanCallback(browser, callback.url);\n\n if (callback.invalid) throw new PixelumeAuthError('invalid_callback');\n if (pending === null || this.#isExpired(pending)) {\n this.#removeSessionHint(browser.sessionStorage);\n throw new PixelumeAuthError(pending === null ? 'invalid_callback' : 'authorization_expired');\n }\n if (\n pending.redirectUri !== this.#config.redirectUri ||\n (callback.code !== null && !/^[A-Za-z0-9_-]{43,128}$/u.test(callback.code))\n ) {\n throw new PixelumeAuthError('invalid_callback');\n }\n const returnedState = singleParameter(callback.url, 'state');\n if (returnedState === null || returnedState !== pending.state) {\n this.#removeSessionHint(browser.sessionStorage);\n throw new PixelumeAuthError('state_mismatch');\n }\n if (callback.error !== null) {\n this.#removeSessionHint(browser.sessionStorage);\n if (callback.error === 'access_denied') {\n this.#setState(anonymous('cancelled'));\n return;\n }\n throw new PixelumeAuthError('invalid_callback');\n }\n if (callback.code === null) throw new PixelumeAuthError('invalid_callback');\n\n this.#setState(Object.freeze({ status: 'exchanging', player: null }));\n const response = await this.#http.request({\n method: 'POST',\n path: '/v1/oauth/token',\n credentials: 'omit',\n form: {\n grant_type: 'authorization_code',\n code: callback.code,\n client_id: this.#requireClientId(),\n redirect_uri: pending.redirectUri,\n code_verifier: pending.verifier,\n },\n parse: parseOAuthTokenResponse,\n });\n this.#session.start(response.data);\n const player = await this.getPlayer();\n if (player !== null) {\n const scope = response.data.scopes.includes('saves') ? 'profile saves' : 'profile';\n this.#writeSessionHint(browser.sessionStorage, scope);\n }\n }\n\n async #fetchPlayer(options: PixelumeRequestOptions): Promise<PixelumePlayer | null> {\n try {\n const response = await this.#session.authorizedRequest({\n method: 'GET',\n path: '/v1/oauth/userinfo',\n signal: options.signal,\n timeoutMs: options.timeoutMs,\n parse: parsePlayerEnvelope,\n });\n const player = Object.freeze({ ...response.data.player });\n const current = this.#state;\n // Keep the same object when nothing changed, so renewals do not look like a new login.\n if (current.status === 'authenticated' && samePlayer(current.player, player)) {\n return current.player;\n }\n this.#setState(Object.freeze({ status: 'authenticated', player }));\n return player;\n } catch (error) {\n // The session ended (expired/revoked); the state already reflects it.\n if (error instanceof PixelumeAuthError) return null;\n throw error;\n }\n }\n\n async #doLogout(options: PixelumeRequestOptions): Promise<void> {\n const token = this.#session.reset();\n const browser = this.#optionalBrowser();\n if (browser) {\n this.#removePending(browser.sessionStorage);\n this.#removeSessionHint(browser.sessionStorage);\n }\n this.#setState(anonymous('logged_out'));\n if (token === null) return;\n // Revoking any token of the family revokes all of it; the refresh token is the preferred hint.\n await this.#http.request({\n method: 'POST',\n path: '/v1/oauth/revoke',\n credentials: 'omit',\n form: {\n token: token.refreshToken ?? token.accessToken,\n token_type_hint: token.refreshToken === null ? 'access_token' : 'refresh_token',\n client_id: this.#requireClientId(),\n },\n signal: options.signal,\n timeoutMs: options.timeoutMs,\n parse: parseEmptyResponse,\n });\n }\n\n #readCallback(value: string): {\n readonly url: URL;\n readonly code: string | null;\n readonly error: string | null;\n readonly invalid: boolean;\n } | null {\n const redirectUri = this.#config.redirectUri;\n if (redirectUri === null) return null;\n const current = new URL(value);\n const expected = new URL(redirectUri);\n if (current.origin !== expected.origin || current.pathname !== expected.pathname) return null;\n const hasCallbackParameter = CALLBACK_PARAMETERS.some((key) => current.searchParams.has(key));\n if (!hasCallbackParameter) return null;\n const code = singleParameter(current, 'code');\n const error = singleParameter(current, 'error');\n return {\n url: current,\n code,\n error,\n invalid: !sameCallbackBase(current, expected) || (code === null) === (error === null),\n };\n }\n\n #cleanCallback(browser: PixelumeBrowserAdapter, url: URL): void {\n const clean = new URL(url.toString());\n for (const key of CALLBACK_PARAMETERS) clean.searchParams.delete(key);\n browser.replaceUrl(`${clean.pathname}${clean.search}${clean.hash}`);\n }\n\n #browser(): PixelumeBrowserAdapter {\n const browser = this.#optionalBrowser();\n if (!browser) throw new PixelumeAuthError('browser_unavailable');\n if (\n typeof browser.crypto.getRandomValues !== 'function' ||\n typeof browser.crypto.subtle.digest !== 'function'\n ) {\n throw new PixelumeAuthError('crypto_unavailable');\n }\n return browser;\n }\n\n #optionalBrowser(): PixelumeBrowserAdapter | null {\n if (this.#config.browser) return this.#config.browser;\n try {\n if (\n typeof globalThis.location === 'undefined' ||\n typeof globalThis.history === 'undefined' ||\n typeof globalThis.sessionStorage === 'undefined' ||\n typeof globalThis.crypto === 'undefined'\n ) {\n return null;\n }\n return {\n currentUrl: () => globalThis.location.href,\n navigate: (url) => {\n globalThis.location.assign(url);\n },\n replaceUrl: (url) => {\n globalThis.history.replaceState(globalThis.history.state, '', url);\n },\n sessionStorage: globalThis.sessionStorage,\n crypto: globalThis.crypto,\n };\n } catch (cause) {\n throw new PixelumeAuthError('storage_unavailable', { cause });\n }\n }\n\n #requireClientId(): string {\n if (this.#config.clientId === null) {\n throw new PixelumeConfigError('Pixelume Auth requires clientId or gameId');\n }\n return this.#config.clientId;\n }\n\n #requireRedirectUri(): string {\n if (this.#config.redirectUri === null) {\n throw new PixelumeAuthError('browser_unavailable');\n }\n return this.#config.redirectUri;\n }\n\n #pendingKey(): string {\n return `pixelume.oauth.pending.${this.#config.clientId ?? 'unconfigured'}`;\n }\n\n #hintKey(): string {\n return `pixelume.oauth.session.${this.#config.clientId ?? 'unconfigured'}`;\n }\n\n #readPending(storage: PixelumeSessionStorage): PendingAuthorization | null {\n try {\n const raw = storage.getItem(this.#pendingKey());\n if (raw === null) return null;\n const value: unknown = JSON.parse(raw);\n if (!isPendingAuthorization(value)) {\n storage.removeItem(this.#pendingKey());\n return null;\n }\n return value;\n } catch (cause) {\n throw new PixelumeAuthError('storage_unavailable', { cause });\n }\n }\n\n #writePending(storage: PixelumeSessionStorage, pending: PendingAuthorization): void {\n try {\n storage.setItem(this.#pendingKey(), JSON.stringify(pending));\n } catch (cause) {\n throw new PixelumeAuthError('storage_unavailable', { cause });\n }\n }\n\n #removePending(storage: PixelumeSessionStorage): void {\n try {\n storage.removeItem(this.#pendingKey());\n } catch (cause) {\n throw new PixelumeAuthError('storage_unavailable', { cause });\n }\n }\n\n /** The hint is non-secret: only \"a session existed\" and the scope that was granted. */\n #readSessionHint(storage: PixelumeSessionStorage): PixelumeScopeRequest | null {\n try {\n const value = storage.getItem(this.#hintKey());\n if (value === '1' || value === 'profile') return 'profile';\n if (value === 'profile saves') return 'profile saves';\n return null;\n } catch (cause) {\n throw new PixelumeAuthError('storage_unavailable', { cause });\n }\n }\n\n #writeSessionHint(storage: PixelumeSessionStorage, scope: PixelumeScopeRequest): void {\n try {\n storage.setItem(this.#hintKey(), scope);\n } catch (cause) {\n throw new PixelumeAuthError('storage_unavailable', { cause });\n }\n }\n\n #removeSessionHint(storage: PixelumeSessionStorage): void {\n try {\n storage.removeItem(this.#hintKey());\n } catch (cause) {\n throw new PixelumeAuthError('storage_unavailable', { cause });\n }\n }\n\n #isExpired(pending: PendingAuthorization): boolean {\n const now = Date.now();\n return (\n pending.createdAt > now + 60_000 || pending.createdAt + AUTHORIZATION_REQUEST_TTL_MS < now\n );\n }\n\n #setState(state: PixelumeAuthState): void {\n const previous = this.#state;\n if (sameState(previous, state)) return;\n this.#state = state;\n for (const listener of [...this.#listeners]) listener(state, previous);\n }\n}\n\nfunction anonymous(\n reason: PixelumeAnonymousReason,\n error: PixelumeError | null = null,\n): PixelumeAuthState {\n return Object.freeze({ status: 'anonymous', player: null, reason, error });\n}\n\nfunction samePlayer(left: PixelumePlayer, right: PixelumePlayer): boolean {\n return (\n left.id === right.id &&\n left.displayName === right.displayName &&\n left.avatarUrl === right.avatarUrl\n );\n}\n\nfunction isPendingAuthorization(value: unknown): value is PendingAuthorization {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;\n const pending = value as Record<string, unknown>;\n return (\n pending.version === 1 &&\n typeof pending.state === 'string' &&\n /^[A-Za-z0-9_-]{43}$/u.test(pending.state) &&\n typeof pending.verifier === 'string' &&\n /^[A-Za-z0-9_-]{43}$/u.test(pending.verifier) &&\n typeof pending.redirectUri === 'string' &&\n typeof pending.createdAt === 'number' &&\n Number.isFinite(pending.createdAt)\n );\n}\n\nfunction sameCallbackBase(current: URL, expected: URL): boolean {\n if (current.origin !== expected.origin || current.pathname !== expected.pathname) return false;\n const currentBase = new URL(current.toString());\n for (const key of CALLBACK_PARAMETERS) currentBase.searchParams.delete(key);\n currentBase.hash = '';\n const expectedBase = new URL(expected.toString());\n expectedBase.hash = '';\n return currentBase.toString() === expectedBase.toString();\n}\n\nfunction singleParameter(url: URL, name: string): string | null {\n const values = url.searchParams.getAll(name);\n if (values.length !== 1 || values[0] === '') return null;\n return values[0] ?? null;\n}\n\nfunction randomBase64Url(crypto: Crypto): string {\n const bytes = crypto.getRandomValues(new Uint8Array(32));\n return bytesToBase64Url(bytes);\n}\n\nasync function sha256Base64Url(crypto: Crypto, value: string): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value));\n return bytesToBase64Url(new Uint8Array(digest));\n}\n\nfunction bytesToBase64Url(bytes: Uint8Array): string {\n let binary = '';\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/u, '');\n}\n\nfunction sameState(left: PixelumeAuthState, right: PixelumeAuthState): boolean {\n if (left.status !== right.status) return false;\n if (left.status === 'authenticated' && right.status === 'authenticated') {\n return left.player === right.player;\n }\n if (left.status === 'anonymous' && right.status === 'anonymous') {\n return left.reason === right.reason && left.error === right.error;\n }\n return true;\n}\n"]}
@@ -0,0 +1,91 @@
1
+ import { type OAuthTokenResponseWire } from '../../contracts/index.js';
2
+ import { PixelumeAuthError, type PixelumeError } from '../../core/errors.js';
3
+ import type { PixelumeRequestOptions } from '../../core/types.js';
4
+ import type { HttpClient } from '../../http/HttpClient.js';
5
+ import type { HttpRequest, HttpResponse } from '../../http/types.js';
6
+ /**
7
+ * Renew this long before the access token expires (API guide: `expires_in − 60 s`). For very short
8
+ * tokens (the API allows down to 60 s) the lead is capped at half the lifetime.
9
+ */
10
+ export declare const REFRESH_LEAD_MS = 60000;
11
+ /** Never schedule a renewal sooner than this, even for very short access tokens. */
12
+ export declare const MIN_REFRESH_DELAY_MS = 10000;
13
+ /** OAuth scopes the API can grant. `profile` is always present. */
14
+ export type PixelumeScope = 'profile' | 'saves';
15
+ /** Snapshot of the token lifecycle of the signed-in player. Never contains tokens. */
16
+ export interface PixelumeSessionState {
17
+ /** `true` while the SDK holds credentials for a player. */
18
+ readonly active: boolean;
19
+ /** Scopes granted to the current access token (empty when inactive). */
20
+ readonly scopes: readonly PixelumeScope[];
21
+ /**
22
+ * Whether the API issued a refresh token. When `false` (legacy authorization) the session ends
23
+ * when the access token expires and the player must sign in again.
24
+ */
25
+ readonly renewable: boolean;
26
+ /** Approximate expiry of the current access token (local clock, ms since epoch), or `null`. */
27
+ readonly accessTokenExpiresAt: number | null;
28
+ /** A renewal request is in flight. */
29
+ readonly renewing: boolean;
30
+ /**
31
+ * Last transient renewal failure (network, timeout, 5xx or rate limit). The session is still
32
+ * recoverable and the SDK will retry; cleared after a successful renewal.
33
+ */
34
+ readonly error: PixelumeError | null;
35
+ }
36
+ export type PixelumeSessionListener = (state: PixelumeSessionState, previous: PixelumeSessionState) => void;
37
+ /** Registers a callback for "the page may have been suspended" signals. Returns an unsubscribe. */
38
+ export type ResumeSource = (onResume: () => void) => () => void;
39
+ /** Timer/clock seam; tests drive it with fake timers. */
40
+ interface Clock {
41
+ now(): number;
42
+ monotonic(): number;
43
+ }
44
+ export interface SessionManagerOptions {
45
+ readonly http: HttpClient;
46
+ readonly clientId: () => string;
47
+ readonly resume: ResumeSource | null;
48
+ /** Called once when the session ends for good (not on logout/reset). */
49
+ readonly onEnded: (error: PixelumeAuthError) => void;
50
+ }
51
+ /**
52
+ * Owns the player's OAuth credentials for one client instance: memory-only storage, proactive
53
+ * and on-demand renewal with refresh-token rotation, single-flight coordination, one retry after
54
+ * `401`, and timers/listeners that are released on reset or dispose.
55
+ *
56
+ * Tokens never leave this class except as the `Authorization` header and the token/revoke form
57
+ * bodies sent to the API.
58
+ */
59
+ export declare class SessionManager {
60
+ #private;
61
+ constructor(options: SessionManagerOptions, clock?: Clock);
62
+ get state(): PixelumeSessionState;
63
+ get active(): boolean;
64
+ subscribe(listener: PixelumeSessionListener): () => void;
65
+ /** Installs the credentials returned by the authorization-code exchange. */
66
+ start(token: OAuthTokenResponseWire): void;
67
+ /**
68
+ * Discards credentials without contacting the API (logout does the revocation) and returns
69
+ * them so the caller can revoke. Cancels timers, in-flight renewals and listeners.
70
+ */
71
+ reset(): {
72
+ readonly accessToken: string;
73
+ readonly refreshToken: string | null;
74
+ } | null;
75
+ /** Releases timers and listeners for good. Credentials are dropped from memory. */
76
+ dispose(): void;
77
+ /**
78
+ * Renews now (single-flight). Resolves after a successful rotation; rejects with the transient
79
+ * error (session kept) or with `PixelumeAuthError` when the session ended.
80
+ */
81
+ refresh(options?: PixelumeRequestOptions): Promise<void>;
82
+ /**
83
+ * Sends a Bearer request with a fresh access token. On `401` it renews once (shared with any
84
+ * concurrent caller) and repeats the very same request once. A `401` is returned by the API
85
+ * before the operation runs, so the repetition never applies a write twice.
86
+ */
87
+ authorizedRequest<T>(request: Omit<HttpRequest<T>, 'headers' | 'credentials'>): Promise<HttpResponse<T>>;
88
+ }
89
+ /** Default resume signals in a browser: tab visible again, back/forward cache restore, online. */
90
+ export declare function browserResumeSource(): ResumeSource | null;
91
+ export {};