@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.
- package/CHANGELOG.md +43 -1
- package/README.md +73 -19
- package/dist/contracts/index.d.ts +42 -2
- package/dist/contracts/index.js +107 -8
- package/dist/contracts/index.js.map +1 -1
- package/dist/core/PixelumeClient.d.ts +12 -2
- package/dist/core/PixelumeClient.js +16 -2
- package/dist/core/PixelumeClient.js.map +1 -1
- package/dist/core/config.d.ts +2 -1
- package/dist/core/config.js +8 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/errors.d.ts +44 -2
- package/dist/core/errors.js +30 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/types.d.ts +12 -0
- package/dist/core/types.js.map +1 -1
- package/dist/http/HttpClient.js +1 -0
- package/dist/http/HttpClient.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/auth/AuthModule.d.ts +28 -3
- package/dist/modules/auth/AuthModule.js +82 -40
- package/dist/modules/auth/AuthModule.js.map +1 -1
- package/dist/modules/auth/SessionManager.d.ts +91 -0
- package/dist/modules/auth/SessionManager.js +475 -0
- package/dist/modules/auth/SessionManager.js.map +1 -0
- package/dist/modules/saves/SavesModule.d.ts +100 -0
- package/dist/modules/saves/SavesModule.js +238 -0
- package/dist/modules/saves/SavesModule.js.map +1 -0
- package/dist/ui/widget.js +61 -4
- package/dist/ui/widget.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { parseEmptyResponse, parseOAuthTokenResponse, parsePlayerEnvelope, } from '../../contracts/index.js';
|
|
2
|
-
import { PixelumeAuthError, PixelumeConfigError,
|
|
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
|
-
#
|
|
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 !==
|
|
34
|
-
return Promise.reject(new PixelumeConfigError('Pixelume Auth
|
|
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.#
|
|
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
|
|
68
|
-
this.#
|
|
69
|
-
|
|
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
|
|
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.#
|
|
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
|
-
|
|
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.#
|
|
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
|
-
|
|
189
|
-
|
|
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.#
|
|
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: {
|
|
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
|
-
|
|
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(),
|
|
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 {};
|