@pixelume/sdk 0.3.0 → 0.5.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.
@@ -0,0 +1,475 @@
1
+ import { parseEmptyResponse, parseOAuthTokenResponse, } from '../../contracts/index.js';
2
+ import { PixelumeAbortError, PixelumeAuthError, PixelumeConfigError, PixelumeHttpError, isPixelumeError, } from '../../core/errors.js';
3
+ /**
4
+ * Renew this long before the access token expires (API guide: `expires_in − 60 s`). For very short
5
+ * tokens (the API allows down to 60 s) the lead is capped at half the lifetime.
6
+ */
7
+ export const REFRESH_LEAD_MS = 60_000;
8
+ /** Never schedule a renewal sooner than this, even for very short access tokens. */
9
+ export const MIN_REFRESH_DELAY_MS = 10_000;
10
+ /** Random spread subtracted from the schedule so many tabs/games do not renew in lockstep. */
11
+ const REFRESH_JITTER_MS = 5_000;
12
+ /** Below this remaining lifetime a request first waits for a renewal (or fails). */
13
+ const ACCESS_TOKEN_SKEW_MS = 5_000;
14
+ /** Single retry with the same refresh token after a network/5xx/timeout failure (API: ≤ ~5 s). */
15
+ const TRANSIENT_RETRY_DELAY_MS = 2_000;
16
+ /** Backoff after a renewal still fails transiently; retried earlier on `online`/visibility. */
17
+ const BACKOFF_MS = [15_000, 30_000, 60_000, 120_000, 300_000];
18
+ const systemClock = {
19
+ now: () => Date.now(),
20
+ // Looked up per call so fake timers installed after import are honoured in tests.
21
+ monotonic: () => {
22
+ const performance = Reflect.get(globalThis, 'performance');
23
+ return typeof performance?.now === 'function' ? performance.now() : Date.now();
24
+ },
25
+ };
26
+ const INACTIVE = Object.freeze({
27
+ active: false,
28
+ scopes: Object.freeze([]),
29
+ renewable: false,
30
+ accessTokenExpiresAt: null,
31
+ renewing: false,
32
+ error: null,
33
+ });
34
+ /**
35
+ * Owns the player's OAuth credentials for one client instance: memory-only storage, proactive
36
+ * and on-demand renewal with refresh-token rotation, single-flight coordination, one retry after
37
+ * `401`, and timers/listeners that are released on reset or dispose.
38
+ *
39
+ * Tokens never leave this class except as the `Authorization` header and the token/revoke form
40
+ * bodies sent to the API.
41
+ */
42
+ export class SessionManager {
43
+ #options;
44
+ #clock;
45
+ #listeners = new Set();
46
+ #credentials = null;
47
+ /** Incremented whenever credentials are discarded; stale async work compares against it. */
48
+ #epoch = 0;
49
+ #refreshing = null;
50
+ #refreshAbort = null;
51
+ #timer = null;
52
+ #backoffIndex = 0;
53
+ #transientError = null;
54
+ #removeResume = null;
55
+ #state = INACTIVE;
56
+ #disposed = false;
57
+ constructor(options, clock = systemClock) {
58
+ this.#options = options;
59
+ this.#clock = clock;
60
+ }
61
+ get state() {
62
+ return this.#state;
63
+ }
64
+ get active() {
65
+ return this.#credentials !== null;
66
+ }
67
+ subscribe(listener) {
68
+ this.#listeners.add(listener);
69
+ return () => this.#listeners.delete(listener);
70
+ }
71
+ /** Installs the credentials returned by the authorization-code exchange. */
72
+ start(token) {
73
+ this.#assertUsable();
74
+ this.#discard();
75
+ this.#install(token);
76
+ this.#removeResume =
77
+ this.#options.resume?.(() => {
78
+ this.#onResume();
79
+ }) ?? null;
80
+ }
81
+ /**
82
+ * Discards credentials without contacting the API (logout does the revocation) and returns
83
+ * them so the caller can revoke. Cancels timers, in-flight renewals and listeners.
84
+ */
85
+ reset() {
86
+ const credentials = this.#credentials;
87
+ this.#discard();
88
+ return (credentials && {
89
+ accessToken: credentials.accessToken,
90
+ refreshToken: credentials.refreshToken,
91
+ });
92
+ }
93
+ /** Releases timers and listeners for good. Credentials are dropped from memory. */
94
+ dispose() {
95
+ if (this.#disposed)
96
+ return;
97
+ this.#discard();
98
+ this.#disposed = true;
99
+ this.#listeners.clear();
100
+ }
101
+ /**
102
+ * Renews now (single-flight). Resolves after a successful rotation; rejects with the transient
103
+ * error (session kept) or with `PixelumeAuthError` when the session ended.
104
+ */
105
+ refresh(options = {}) {
106
+ this.#assertUsable();
107
+ const credentials = this.#credentials;
108
+ if (credentials === null)
109
+ return Promise.reject(new PixelumeAuthError('not_authenticated'));
110
+ if (credentials.refreshToken === null) {
111
+ // Legacy authorization without refresh token: nothing to renew, the session stays as is.
112
+ return Promise.reject(new PixelumeAuthError('renewal_rejected'));
113
+ }
114
+ return waitFor(this.#sharedRefresh(), options.signal, {
115
+ method: 'POST',
116
+ path: '/v1/oauth/token',
117
+ });
118
+ }
119
+ /**
120
+ * Sends a Bearer request with a fresh access token. On `401` it renews once (shared with any
121
+ * concurrent caller) and repeats the very same request once. A `401` is returned by the API
122
+ * before the operation runs, so the repetition never applies a write twice.
123
+ */
124
+ async authorizedRequest(request) {
125
+ let credentials = await this.#usableCredentials(request);
126
+ try {
127
+ return await this.#send(request, credentials);
128
+ }
129
+ catch (error) {
130
+ if (!(error instanceof PixelumeHttpError) || error.status !== 401)
131
+ throw error;
132
+ const current = this.#credentials;
133
+ if (current === null)
134
+ throw new PixelumeAuthError('session_expired', { cause: error });
135
+ if (current.accessToken === credentials.accessToken) {
136
+ if (current.refreshToken === null)
137
+ throw this.#end('session_expired', error, false);
138
+ await waitFor(this.#sharedRefresh(), request.signal, request);
139
+ }
140
+ credentials = this.#credentials ?? credentials;
141
+ }
142
+ try {
143
+ return await this.#send(request, credentials);
144
+ }
145
+ catch (error) {
146
+ if (error instanceof PixelumeHttpError && error.status === 401) {
147
+ // Rejected again with a token just issued by the API: stop instead of looping.
148
+ throw this.#end('session_expired', error, true);
149
+ }
150
+ throw error;
151
+ }
152
+ }
153
+ async #usableCredentials(request) {
154
+ this.#assertUsable();
155
+ const credentials = this.#credentials;
156
+ if (credentials === null)
157
+ throw new PixelumeAuthError('not_authenticated');
158
+ const remaining = this.#remaining(credentials);
159
+ if (remaining > ACCESS_TOKEN_SKEW_MS) {
160
+ if (remaining < credentials.leadMs)
161
+ this.#backgroundRefresh();
162
+ return credentials;
163
+ }
164
+ if (credentials.refreshToken === null)
165
+ throw this.#end('session_expired', null, false);
166
+ await waitFor(this.#sharedRefresh(), request.signal, request);
167
+ const renewed = this.#credentials;
168
+ if (renewed === null)
169
+ throw new PixelumeAuthError('session_expired');
170
+ return renewed;
171
+ }
172
+ #send(request, credentials) {
173
+ return this.#options.http.request({
174
+ ...request,
175
+ credentials: 'omit',
176
+ headers: { authorization: `Bearer ${credentials.accessToken}` },
177
+ });
178
+ }
179
+ #sharedRefresh() {
180
+ if (this.#refreshing === null) {
181
+ const refreshing = this.#runRefresh().finally(() => {
182
+ if (this.#refreshing === refreshing)
183
+ this.#refreshing = null;
184
+ this.#publish();
185
+ });
186
+ this.#refreshing = refreshing;
187
+ this.#publish();
188
+ }
189
+ return this.#refreshing;
190
+ }
191
+ #backgroundRefresh() {
192
+ if (this.#refreshing !== null || this.#credentials?.refreshToken == null)
193
+ return;
194
+ this.#sharedRefresh().catch(() => undefined);
195
+ }
196
+ async #runRefresh() {
197
+ const epoch = this.#epoch;
198
+ const controller = new AbortController();
199
+ this.#refreshAbort = controller;
200
+ this.#clearTimer();
201
+ try {
202
+ for (let attempt = 0;; attempt += 1) {
203
+ const credentials = this.#credentials;
204
+ if (credentials?.refreshToken == null || epoch !== this.#epoch) {
205
+ throw new PixelumeAuthError('session_expired');
206
+ }
207
+ let token;
208
+ try {
209
+ const response = await this.#options.http.request({
210
+ method: 'POST',
211
+ path: '/v1/oauth/token',
212
+ credentials: 'omit',
213
+ form: {
214
+ grant_type: 'refresh_token',
215
+ refresh_token: credentials.refreshToken,
216
+ client_id: this.#options.clientId(),
217
+ },
218
+ signal: controller.signal,
219
+ parse: parseOAuthTokenResponse,
220
+ });
221
+ token = response.data;
222
+ }
223
+ catch (error) {
224
+ if (epoch !== this.#epoch)
225
+ throw new PixelumeAuthError('session_expired', { cause: error });
226
+ const outcome = classifyRefreshFailure(error);
227
+ if (outcome.kind === 'retry_once' && attempt === 0) {
228
+ try {
229
+ await sleep(TRANSIENT_RETRY_DELAY_MS, controller.signal);
230
+ }
231
+ catch (cause) {
232
+ throw new PixelumeAuthError('session_expired', { cause });
233
+ }
234
+ continue;
235
+ }
236
+ if (outcome.kind !== 'terminal') {
237
+ this.#transientError = error;
238
+ this.#scheduleBackoff();
239
+ throw error;
240
+ }
241
+ throw this.#end(outcome.reason, error, false);
242
+ }
243
+ if (epoch !== this.#epoch) {
244
+ // Logged out while the request was in flight: the API already rotated. Revoke the new
245
+ // family member so nothing issued after logout stays usable.
246
+ this.#revokeQuietly(token.refresh_token ?? token.access_token, token.refresh_token);
247
+ throw new PixelumeAuthError('session_expired');
248
+ }
249
+ this.#install(token);
250
+ return;
251
+ }
252
+ }
253
+ finally {
254
+ if (this.#refreshAbort === controller)
255
+ this.#refreshAbort = null;
256
+ }
257
+ }
258
+ #install(token) {
259
+ const lifetime = token.expires_in * 1_000;
260
+ this.#credentials = {
261
+ accessToken: token.access_token,
262
+ expiresAt: this.#clock.now() + lifetime,
263
+ expiresAtMonotonic: this.#clock.monotonic() + lifetime,
264
+ leadMs: Math.min(REFRESH_LEAD_MS, lifetime / 2),
265
+ refreshToken: token.refresh_token ?? null,
266
+ scopes: Object.freeze([...token.scopes]),
267
+ };
268
+ this.#transientError = null;
269
+ this.#backoffIndex = 0;
270
+ this.#scheduleRenewal();
271
+ this.#publish();
272
+ }
273
+ /** Ends the session for good and reports it. Returns the error for the caller to throw. */
274
+ #end(reason, cause, revoke) {
275
+ const error = new PixelumeAuthError(reason, cause === null ? undefined : { cause });
276
+ const credentials = this.#credentials;
277
+ if (credentials === null)
278
+ return error;
279
+ this.#discard();
280
+ if (revoke) {
281
+ this.#revokeQuietly(credentials.refreshToken ?? credentials.accessToken, credentials.refreshToken);
282
+ }
283
+ this.#options.onEnded(error);
284
+ return error;
285
+ }
286
+ #discard() {
287
+ this.#epoch += 1;
288
+ this.#credentials = null;
289
+ this.#transientError = null;
290
+ this.#backoffIndex = 0;
291
+ this.#clearTimer();
292
+ this.#refreshAbort?.abort(new DOMException('Session reset', 'AbortError'));
293
+ this.#refreshAbort = null;
294
+ this.#refreshing = null;
295
+ this.#removeResume?.();
296
+ this.#removeResume = null;
297
+ this.#publish();
298
+ }
299
+ #revokeQuietly(token, refreshToken) {
300
+ this.#options.http
301
+ .request({
302
+ method: 'POST',
303
+ path: '/v1/oauth/revoke',
304
+ credentials: 'omit',
305
+ form: {
306
+ token,
307
+ token_type_hint: refreshToken ? 'refresh_token' : 'access_token',
308
+ client_id: this.#options.clientId(),
309
+ },
310
+ parse: parseEmptyResponse,
311
+ })
312
+ .catch(() => undefined);
313
+ }
314
+ #remaining(credentials) {
315
+ return Math.min(credentials.expiresAt - this.#clock.now(), credentials.expiresAtMonotonic - this.#clock.monotonic());
316
+ }
317
+ #scheduleRenewal() {
318
+ const credentials = this.#credentials;
319
+ if (credentials?.refreshToken == null)
320
+ return;
321
+ const jitter = Math.floor(Math.random() * REFRESH_JITTER_MS);
322
+ const delay = Math.max(MIN_REFRESH_DELAY_MS, this.#remaining(credentials) - credentials.leadMs - jitter);
323
+ this.#setTimer(delay);
324
+ }
325
+ #scheduleBackoff() {
326
+ const base = BACKOFF_MS[Math.min(this.#backoffIndex, BACKOFF_MS.length - 1)] ?? 300_000;
327
+ this.#backoffIndex += 1;
328
+ this.#setTimer(base + Math.floor(Math.random() * (base / 5)));
329
+ }
330
+ #setTimer(delay) {
331
+ this.#clearTimer();
332
+ const timer = setTimeout(() => {
333
+ this.#timer = null;
334
+ this.#backgroundRefresh();
335
+ }, delay);
336
+ // Node.js/Deno: a pending renewal must not keep a script or test runner alive.
337
+ timer.unref?.();
338
+ this.#timer = timer;
339
+ }
340
+ #clearTimer() {
341
+ if (this.#timer !== null)
342
+ clearTimeout(this.#timer);
343
+ this.#timer = null;
344
+ }
345
+ /** After suspension, sleep or reconnection: timers may have fired late or not at all. */
346
+ #onResume() {
347
+ const credentials = this.#credentials;
348
+ if (credentials?.refreshToken == null || this.#refreshing !== null)
349
+ return;
350
+ if (this.#transientError !== null || this.#remaining(credentials) < credentials.leadMs) {
351
+ this.#backgroundRefresh();
352
+ }
353
+ }
354
+ #assertUsable() {
355
+ if (this.#disposed)
356
+ throw new PixelumeConfigError('This Pixelume client was destroyed');
357
+ }
358
+ #publish() {
359
+ const credentials = this.#credentials;
360
+ const next = credentials === null
361
+ ? INACTIVE
362
+ : Object.freeze({
363
+ active: true,
364
+ scopes: credentials.scopes,
365
+ renewable: credentials.refreshToken !== null,
366
+ accessTokenExpiresAt: credentials.expiresAt,
367
+ renewing: this.#refreshing !== null,
368
+ error: this.#transientError,
369
+ });
370
+ const previous = this.#state;
371
+ if (sameSessionState(previous, next))
372
+ return;
373
+ this.#state = next;
374
+ for (const listener of [...this.#listeners])
375
+ listener(next, previous);
376
+ }
377
+ }
378
+ /**
379
+ * Maps a failed `grant_type=refresh_token` request to the policy of the API guide
380
+ * (`pixelume-api/docs/sdk-refresh-migration.md`):
381
+ * - network, timeout, 5xx: unknown outcome → one retry with the same token, then backoff;
382
+ * - 429: the token was not consumed → backoff;
383
+ * - `invalid_grant`, `invalid_client` and every other rejection → terminal.
384
+ */
385
+ function classifyRefreshFailure(error) {
386
+ if (!isPixelumeError(error))
387
+ return { kind: 'terminal', reason: 'session_expired' };
388
+ if (error.kind === 'network' || error.kind === 'timeout')
389
+ return { kind: 'retry_once' };
390
+ if (error instanceof PixelumeHttpError) {
391
+ if (error.status >= 500)
392
+ return { kind: 'retry_once' };
393
+ if (error.status === 429)
394
+ return { kind: 'backoff' };
395
+ const oauthError = error.details?.oauthError;
396
+ if (error.code === 'OAUTH_INVALID_GRANT' || oauthError === 'invalid_grant') {
397
+ return { kind: 'terminal', reason: 'session_expired' };
398
+ }
399
+ if (error.code === 'OAUTH_INVALID_CLIENT' || oauthError === 'invalid_client') {
400
+ return { kind: 'terminal', reason: 'game_unavailable' };
401
+ }
402
+ return { kind: 'terminal', reason: 'renewal_rejected' };
403
+ }
404
+ // A 2xx that does not match the contract: the API may have rotated, the new pair is unusable.
405
+ return { kind: 'terminal', reason: 'session_expired' };
406
+ }
407
+ /** Lets one caller stop waiting for shared work without cancelling it for everybody else. */
408
+ function waitFor(promise, signal, request) {
409
+ if (!signal)
410
+ return promise;
411
+ const info = { method: request.method, path: request.path };
412
+ if (signal.aborted)
413
+ return Promise.reject(new PixelumeAbortError(info, { cause: signal.reason }));
414
+ return new Promise((resolve, reject) => {
415
+ const onAbort = () => {
416
+ reject(new PixelumeAbortError(info, { cause: signal.reason }));
417
+ };
418
+ signal.addEventListener('abort', onAbort, { once: true });
419
+ promise.then(() => {
420
+ signal.removeEventListener('abort', onAbort);
421
+ resolve();
422
+ }, (error) => {
423
+ signal.removeEventListener('abort', onAbort);
424
+ reject(error instanceof Error ? error : new Error(String(error)));
425
+ });
426
+ });
427
+ }
428
+ function sleep(ms, signal) {
429
+ return new Promise((resolve, reject) => {
430
+ if (signal.aborted) {
431
+ reject(signal.reason);
432
+ return;
433
+ }
434
+ const timer = setTimeout(() => {
435
+ signal.removeEventListener('abort', onAbort);
436
+ resolve();
437
+ }, ms);
438
+ timer.unref?.();
439
+ const onAbort = () => {
440
+ clearTimeout(timer);
441
+ reject(signal.reason);
442
+ };
443
+ signal.addEventListener('abort', onAbort, { once: true });
444
+ });
445
+ }
446
+ function sameSessionState(left, right) {
447
+ return (left.active === right.active &&
448
+ left.scopes === right.scopes &&
449
+ left.renewable === right.renewable &&
450
+ left.accessTokenExpiresAt === right.accessTokenExpiresAt &&
451
+ left.renewing === right.renewing &&
452
+ left.error === right.error);
453
+ }
454
+ /** Default resume signals in a browser: tab visible again, back/forward cache restore, online. */
455
+ export function browserResumeSource() {
456
+ const target = globalThis;
457
+ const document = target.document;
458
+ if (typeof target.addEventListener !== 'function' || document === undefined)
459
+ return null;
460
+ return (onResume) => {
461
+ const onVisibility = () => {
462
+ if (document.visibilityState === 'visible')
463
+ onResume();
464
+ };
465
+ document.addEventListener('visibilitychange', onVisibility);
466
+ target.addEventListener?.('online', onResume);
467
+ target.addEventListener?.('pageshow', onResume);
468
+ return () => {
469
+ document.removeEventListener('visibilitychange', onVisibility);
470
+ target.removeEventListener?.('online', onResume);
471
+ target.removeEventListener?.('pageshow', onResume);
472
+ };
473
+ };
474
+ }
475
+ //# sourceMappingURL=SessionManager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SessionManager.js","sourceRoot":"","sources":["../../../src/modules/auth/SessionManager.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n parseEmptyResponse,\n parseOAuthTokenResponse,\n type OAuthTokenResponseWire,\n} from '../../contracts/index.js';\nimport {\n PixelumeAbortError,\n PixelumeAuthError,\n PixelumeConfigError,\n PixelumeHttpError,\n isPixelumeError,\n type PixelumeAuthErrorReason,\n type PixelumeError,\n} from '../../core/errors.js';\nimport type { PixelumeRequestOptions } from '../../core/types.js';\nimport type { HttpClient } from '../../http/HttpClient.js';\nimport type { HttpRequest, HttpResponse } from '../../http/types.js';\n\n/**\n * Renew this long before the access token expires (API guide: `expires_in − 60 s`). For very short\n * tokens (the API allows down to 60 s) the lead is capped at half the lifetime.\n */\nexport const REFRESH_LEAD_MS = 60_000;\n/** Never schedule a renewal sooner than this, even for very short access tokens. */\nexport const MIN_REFRESH_DELAY_MS = 10_000;\n/** Random spread subtracted from the schedule so many tabs/games do not renew in lockstep. */\nconst REFRESH_JITTER_MS = 5_000;\n/** Below this remaining lifetime a request first waits for a renewal (or fails). */\nconst ACCESS_TOKEN_SKEW_MS = 5_000;\n/** Single retry with the same refresh token after a network/5xx/timeout failure (API: ≤ ~5 s). */\nconst TRANSIENT_RETRY_DELAY_MS = 2_000;\n/** Backoff after a renewal still fails transiently; retried earlier on `online`/visibility. */\nconst BACKOFF_MS = [15_000, 30_000, 60_000, 120_000, 300_000] as const;\n\n/** OAuth scopes the API can grant. `profile` is always present. */\nexport type PixelumeScope = 'profile' | 'saves';\n\n/** Snapshot of the token lifecycle of the signed-in player. Never contains tokens. */\nexport interface PixelumeSessionState {\n /** `true` while the SDK holds credentials for a player. */\n readonly active: boolean;\n /** Scopes granted to the current access token (empty when inactive). */\n readonly scopes: readonly PixelumeScope[];\n /**\n * Whether the API issued a refresh token. When `false` (legacy authorization) the session ends\n * when the access token expires and the player must sign in again.\n */\n readonly renewable: boolean;\n /** Approximate expiry of the current access token (local clock, ms since epoch), or `null`. */\n readonly accessTokenExpiresAt: number | null;\n /** A renewal request is in flight. */\n readonly renewing: boolean;\n /**\n * Last transient renewal failure (network, timeout, 5xx or rate limit). The session is still\n * recoverable and the SDK will retry; cleared after a successful renewal.\n */\n readonly error: PixelumeError | null;\n}\n\nexport type PixelumeSessionListener = (\n state: PixelumeSessionState,\n previous: PixelumeSessionState,\n) => void;\n\n/** Registers a callback for \"the page may have been suspended\" signals. Returns an unsubscribe. */\nexport type ResumeSource = (onResume: () => void) => () => void;\n\n/** Timer/clock seam; tests drive it with fake timers. */\ninterface Clock {\n now(): number;\n monotonic(): number;\n}\n\nconst systemClock: Clock = {\n now: () => Date.now(),\n // Looked up per call so fake timers installed after import are honoured in tests.\n monotonic: () => {\n const performance = Reflect.get(globalThis, 'performance') as Performance | undefined;\n return typeof performance?.now === 'function' ? performance.now() : Date.now();\n },\n};\n\ninterface Credentials {\n readonly accessToken: string;\n /** Expiry on both clocks: wall time survives suspension, monotonic survives clock changes. */\n readonly expiresAt: number;\n readonly expiresAtMonotonic: number;\n /** How long before expiry a renewal is due. */\n readonly leadMs: number;\n readonly refreshToken: string | null;\n readonly scopes: readonly PixelumeScope[];\n}\n\nexport interface SessionManagerOptions {\n readonly http: HttpClient;\n readonly clientId: () => string;\n readonly resume: ResumeSource | null;\n /** Called once when the session ends for good (not on logout/reset). */\n readonly onEnded: (error: PixelumeAuthError) => void;\n}\n\nconst INACTIVE: PixelumeSessionState = Object.freeze({\n active: false,\n scopes: Object.freeze([]),\n renewable: false,\n accessTokenExpiresAt: null,\n renewing: false,\n error: null,\n});\n\n/**\n * Owns the player's OAuth credentials for one client instance: memory-only storage, proactive\n * and on-demand renewal with refresh-token rotation, single-flight coordination, one retry after\n * `401`, and timers/listeners that are released on reset or dispose.\n *\n * Tokens never leave this class except as the `Authorization` header and the token/revoke form\n * bodies sent to the API.\n */\nexport class SessionManager {\n readonly #options: SessionManagerOptions;\n readonly #clock: Clock;\n readonly #listeners = new Set<PixelumeSessionListener>();\n #credentials: Credentials | null = null;\n /** Incremented whenever credentials are discarded; stale async work compares against it. */\n #epoch = 0;\n #refreshing: Promise<void> | null = null;\n #refreshAbort: AbortController | null = null;\n #timer: ReturnType<typeof setTimeout> | null = null;\n #backoffIndex = 0;\n #transientError: PixelumeError | null = null;\n #removeResume: (() => void) | null = null;\n #state: PixelumeSessionState = INACTIVE;\n #disposed = false;\n\n constructor(options: SessionManagerOptions, clock: Clock = systemClock) {\n this.#options = options;\n this.#clock = clock;\n }\n\n get state(): PixelumeSessionState {\n return this.#state;\n }\n\n get active(): boolean {\n return this.#credentials !== null;\n }\n\n subscribe(listener: PixelumeSessionListener): () => void {\n this.#listeners.add(listener);\n return () => this.#listeners.delete(listener);\n }\n\n /** Installs the credentials returned by the authorization-code exchange. */\n start(token: OAuthTokenResponseWire): void {\n this.#assertUsable();\n this.#discard();\n this.#install(token);\n this.#removeResume =\n this.#options.resume?.(() => {\n this.#onResume();\n }) ?? null;\n }\n\n /**\n * Discards credentials without contacting the API (logout does the revocation) and returns\n * them so the caller can revoke. Cancels timers, in-flight renewals and listeners.\n */\n reset(): { readonly accessToken: string; readonly refreshToken: string | null } | null {\n const credentials = this.#credentials;\n this.#discard();\n return (\n credentials && {\n accessToken: credentials.accessToken,\n refreshToken: credentials.refreshToken,\n }\n );\n }\n\n /** Releases timers and listeners for good. Credentials are dropped from memory. */\n dispose(): void {\n if (this.#disposed) return;\n this.#discard();\n this.#disposed = true;\n this.#listeners.clear();\n }\n\n /**\n * Renews now (single-flight). Resolves after a successful rotation; rejects with the transient\n * error (session kept) or with `PixelumeAuthError` when the session ended.\n */\n refresh(options: PixelumeRequestOptions = {}): Promise<void> {\n this.#assertUsable();\n const credentials = this.#credentials;\n if (credentials === null) return Promise.reject(new PixelumeAuthError('not_authenticated'));\n if (credentials.refreshToken === null) {\n // Legacy authorization without refresh token: nothing to renew, the session stays as is.\n return Promise.reject(new PixelumeAuthError('renewal_rejected'));\n }\n return waitFor(this.#sharedRefresh(), options.signal, {\n method: 'POST',\n path: '/v1/oauth/token',\n });\n }\n\n /**\n * Sends a Bearer request with a fresh access token. On `401` it renews once (shared with any\n * concurrent caller) and repeats the very same request once. A `401` is returned by the API\n * before the operation runs, so the repetition never applies a write twice.\n */\n async authorizedRequest<T>(\n request: Omit<HttpRequest<T>, 'headers' | 'credentials'>,\n ): Promise<HttpResponse<T>> {\n let credentials = await this.#usableCredentials(request);\n try {\n return await this.#send(request, credentials);\n } catch (error) {\n if (!(error instanceof PixelumeHttpError) || error.status !== 401) throw error;\n const current = this.#credentials;\n if (current === null) throw new PixelumeAuthError('session_expired', { cause: error });\n if (current.accessToken === credentials.accessToken) {\n if (current.refreshToken === null) throw this.#end('session_expired', error, false);\n await waitFor(this.#sharedRefresh(), request.signal, request);\n }\n credentials = this.#credentials ?? credentials;\n }\n try {\n return await this.#send(request, credentials);\n } catch (error) {\n if (error instanceof PixelumeHttpError && error.status === 401) {\n // Rejected again with a token just issued by the API: stop instead of looping.\n throw this.#end('session_expired', error, true);\n }\n throw error;\n }\n }\n\n async #usableCredentials(request: {\n readonly method: string;\n readonly path: string;\n readonly signal?: AbortSignal | undefined;\n }): Promise<Credentials> {\n this.#assertUsable();\n const credentials = this.#credentials;\n if (credentials === null) throw new PixelumeAuthError('not_authenticated');\n const remaining = this.#remaining(credentials);\n if (remaining > ACCESS_TOKEN_SKEW_MS) {\n if (remaining < credentials.leadMs) this.#backgroundRefresh();\n return credentials;\n }\n if (credentials.refreshToken === null) throw this.#end('session_expired', null, false);\n await waitFor(this.#sharedRefresh(), request.signal, request);\n const renewed = this.#credentials;\n if (renewed === null) throw new PixelumeAuthError('session_expired');\n return renewed;\n }\n\n #send<T>(\n request: Omit<HttpRequest<T>, 'headers' | 'credentials'>,\n credentials: Credentials,\n ): Promise<HttpResponse<T>> {\n return this.#options.http.request({\n ...request,\n credentials: 'omit',\n headers: { authorization: `Bearer ${credentials.accessToken}` },\n });\n }\n\n #sharedRefresh(): Promise<void> {\n if (this.#refreshing === null) {\n const refreshing = this.#runRefresh().finally(() => {\n if (this.#refreshing === refreshing) this.#refreshing = null;\n this.#publish();\n });\n this.#refreshing = refreshing;\n this.#publish();\n }\n return this.#refreshing;\n }\n\n #backgroundRefresh(): void {\n if (this.#refreshing !== null || this.#credentials?.refreshToken == null) return;\n this.#sharedRefresh().catch(() => undefined);\n }\n\n async #runRefresh(): Promise<void> {\n const epoch = this.#epoch;\n const controller = new AbortController();\n this.#refreshAbort = controller;\n this.#clearTimer();\n try {\n for (let attempt = 0; ; attempt += 1) {\n const credentials = this.#credentials;\n if (credentials?.refreshToken == null || epoch !== this.#epoch) {\n throw new PixelumeAuthError('session_expired');\n }\n let token: OAuthTokenResponseWire;\n try {\n const response = await this.#options.http.request({\n method: 'POST',\n path: '/v1/oauth/token',\n credentials: 'omit',\n form: {\n grant_type: 'refresh_token',\n refresh_token: credentials.refreshToken,\n client_id: this.#options.clientId(),\n },\n signal: controller.signal,\n parse: parseOAuthTokenResponse,\n });\n token = response.data;\n } catch (error) {\n if (epoch !== this.#epoch)\n throw new PixelumeAuthError('session_expired', { cause: error });\n const outcome = classifyRefreshFailure(error);\n if (outcome.kind === 'retry_once' && attempt === 0) {\n try {\n await sleep(TRANSIENT_RETRY_DELAY_MS, controller.signal);\n } catch (cause) {\n throw new PixelumeAuthError('session_expired', { cause });\n }\n continue;\n }\n if (outcome.kind !== 'terminal') {\n this.#transientError = error as PixelumeError;\n this.#scheduleBackoff();\n throw error;\n }\n throw this.#end(outcome.reason, error, false);\n }\n if (epoch !== this.#epoch) {\n // Logged out while the request was in flight: the API already rotated. Revoke the new\n // family member so nothing issued after logout stays usable.\n this.#revokeQuietly(token.refresh_token ?? token.access_token, token.refresh_token);\n throw new PixelumeAuthError('session_expired');\n }\n this.#install(token);\n return;\n }\n } finally {\n if (this.#refreshAbort === controller) this.#refreshAbort = null;\n }\n }\n\n #install(token: OAuthTokenResponseWire): void {\n const lifetime = token.expires_in * 1_000;\n this.#credentials = {\n accessToken: token.access_token,\n expiresAt: this.#clock.now() + lifetime,\n expiresAtMonotonic: this.#clock.monotonic() + lifetime,\n leadMs: Math.min(REFRESH_LEAD_MS, lifetime / 2),\n refreshToken: token.refresh_token ?? null,\n scopes: Object.freeze([...token.scopes]),\n };\n this.#transientError = null;\n this.#backoffIndex = 0;\n this.#scheduleRenewal();\n this.#publish();\n }\n\n /** Ends the session for good and reports it. Returns the error for the caller to throw. */\n #end(reason: PixelumeAuthErrorReason, cause: unknown, revoke: boolean): PixelumeAuthError {\n const error = new PixelumeAuthError(reason, cause === null ? undefined : { cause });\n const credentials = this.#credentials;\n if (credentials === null) return error;\n this.#discard();\n if (revoke) {\n this.#revokeQuietly(\n credentials.refreshToken ?? credentials.accessToken,\n credentials.refreshToken,\n );\n }\n this.#options.onEnded(error);\n return error;\n }\n\n #discard(): void {\n this.#epoch += 1;\n this.#credentials = null;\n this.#transientError = null;\n this.#backoffIndex = 0;\n this.#clearTimer();\n this.#refreshAbort?.abort(new DOMException('Session reset', 'AbortError'));\n this.#refreshAbort = null;\n this.#refreshing = null;\n this.#removeResume?.();\n this.#removeResume = null;\n this.#publish();\n }\n\n #revokeQuietly(token: string, refreshToken: string | null | undefined): void {\n this.#options.http\n .request({\n method: 'POST',\n path: '/v1/oauth/revoke',\n credentials: 'omit',\n form: {\n token,\n token_type_hint: refreshToken ? 'refresh_token' : 'access_token',\n client_id: this.#options.clientId(),\n },\n parse: parseEmptyResponse,\n })\n .catch(() => undefined);\n }\n\n #remaining(credentials: Credentials): number {\n return Math.min(\n credentials.expiresAt - this.#clock.now(),\n credentials.expiresAtMonotonic - this.#clock.monotonic(),\n );\n }\n\n #scheduleRenewal(): void {\n const credentials = this.#credentials;\n if (credentials?.refreshToken == null) return;\n const jitter = Math.floor(Math.random() * REFRESH_JITTER_MS);\n const delay = Math.max(\n MIN_REFRESH_DELAY_MS,\n this.#remaining(credentials) - credentials.leadMs - jitter,\n );\n this.#setTimer(delay);\n }\n\n #scheduleBackoff(): void {\n const base = BACKOFF_MS[Math.min(this.#backoffIndex, BACKOFF_MS.length - 1)] ?? 300_000;\n this.#backoffIndex += 1;\n this.#setTimer(base + Math.floor(Math.random() * (base / 5)));\n }\n\n #setTimer(delay: number): void {\n this.#clearTimer();\n const timer = setTimeout(() => {\n this.#timer = null;\n this.#backgroundRefresh();\n }, delay);\n // Node.js/Deno: a pending renewal must not keep a script or test runner alive.\n (timer as { unref?: () => void }).unref?.();\n this.#timer = timer;\n }\n\n #clearTimer(): void {\n if (this.#timer !== null) clearTimeout(this.#timer);\n this.#timer = null;\n }\n\n /** After suspension, sleep or reconnection: timers may have fired late or not at all. */\n #onResume(): void {\n const credentials = this.#credentials;\n if (credentials?.refreshToken == null || this.#refreshing !== null) return;\n if (this.#transientError !== null || this.#remaining(credentials) < credentials.leadMs) {\n this.#backgroundRefresh();\n }\n }\n\n #assertUsable(): void {\n if (this.#disposed) throw new PixelumeConfigError('This Pixelume client was destroyed');\n }\n\n #publish(): void {\n const credentials = this.#credentials;\n const next: PixelumeSessionState =\n credentials === null\n ? INACTIVE\n : Object.freeze({\n active: true,\n scopes: credentials.scopes,\n renewable: credentials.refreshToken !== null,\n accessTokenExpiresAt: credentials.expiresAt,\n renewing: this.#refreshing !== null,\n error: this.#transientError,\n });\n const previous = this.#state;\n if (sameSessionState(previous, next)) return;\n this.#state = next;\n for (const listener of [...this.#listeners]) listener(next, previous);\n }\n}\n\ntype RefreshFailure =\n | { readonly kind: 'retry_once' }\n | { readonly kind: 'backoff' }\n | { readonly kind: 'terminal'; readonly reason: PixelumeAuthErrorReason };\n\n/**\n * Maps a failed `grant_type=refresh_token` request to the policy of the API guide\n * (`pixelume-api/docs/sdk-refresh-migration.md`):\n * - network, timeout, 5xx: unknown outcome → one retry with the same token, then backoff;\n * - 429: the token was not consumed → backoff;\n * - `invalid_grant`, `invalid_client` and every other rejection → terminal.\n */\nfunction classifyRefreshFailure(error: unknown): RefreshFailure {\n if (!isPixelumeError(error)) return { kind: 'terminal', reason: 'session_expired' };\n if (error.kind === 'network' || error.kind === 'timeout') return { kind: 'retry_once' };\n if (error instanceof PixelumeHttpError) {\n if (error.status >= 500) return { kind: 'retry_once' };\n if (error.status === 429) return { kind: 'backoff' };\n const oauthError = error.details?.oauthError;\n if (error.code === 'OAUTH_INVALID_GRANT' || oauthError === 'invalid_grant') {\n return { kind: 'terminal', reason: 'session_expired' };\n }\n if (error.code === 'OAUTH_INVALID_CLIENT' || oauthError === 'invalid_client') {\n return { kind: 'terminal', reason: 'game_unavailable' };\n }\n return { kind: 'terminal', reason: 'renewal_rejected' };\n }\n // A 2xx that does not match the contract: the API may have rotated, the new pair is unusable.\n return { kind: 'terminal', reason: 'session_expired' };\n}\n\n/** Lets one caller stop waiting for shared work without cancelling it for everybody else. */\nfunction waitFor(\n promise: Promise<void>,\n signal: AbortSignal | undefined,\n request: { readonly method: string; readonly path: string },\n): Promise<void> {\n if (!signal) return promise;\n const info = { method: request.method, path: request.path };\n if (signal.aborted) return Promise.reject(new PixelumeAbortError(info, { cause: signal.reason }));\n return new Promise<void>((resolve, reject) => {\n const onAbort = (): void => {\n reject(new PixelumeAbortError(info, { cause: signal.reason }));\n };\n signal.addEventListener('abort', onAbort, { once: true });\n promise.then(\n () => {\n signal.removeEventListener('abort', onAbort);\n resolve();\n },\n (error: unknown) => {\n signal.removeEventListener('abort', onAbort);\n reject(error instanceof Error ? error : new Error(String(error)));\n },\n );\n });\n}\n\nfunction sleep(ms: number, signal: AbortSignal): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n if (signal.aborted) {\n reject(signal.reason as Error);\n return;\n }\n const timer = setTimeout(() => {\n signal.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n (timer as { unref?: () => void }).unref?.();\n const onAbort = (): void => {\n clearTimeout(timer);\n reject(signal.reason as Error);\n };\n signal.addEventListener('abort', onAbort, { once: true });\n });\n}\n\nfunction sameSessionState(left: PixelumeSessionState, right: PixelumeSessionState): boolean {\n return (\n left.active === right.active &&\n left.scopes === right.scopes &&\n left.renewable === right.renewable &&\n left.accessTokenExpiresAt === right.accessTokenExpiresAt &&\n left.renewing === right.renewing &&\n left.error === right.error\n );\n}\n\n/** Default resume signals in a browser: tab visible again, back/forward cache restore, online. */\nexport function browserResumeSource(): ResumeSource | null {\n const target = globalThis as unknown as Partial<\n Pick<Window, 'addEventListener' | 'removeEventListener'>\n > & { document?: Document };\n const document = target.document;\n if (typeof target.addEventListener !== 'function' || document === undefined) return null;\n return (onResume) => {\n const onVisibility = (): void => {\n if (document.visibilityState === 'visible') onResume();\n };\n document.addEventListener('visibilitychange', onVisibility);\n target.addEventListener?.('online', onResume);\n target.addEventListener?.('pageshow', onResume);\n return () => {\n document.removeEventListener('visibilitychange', onVisibility);\n target.removeEventListener?.('online', onResume);\n target.removeEventListener?.('pageshow', onResume);\n };\n };\n}\n"]}
@@ -0,0 +1,100 @@
1
+ import { type ResolvedConfig } from '../../core/config.js';
2
+ import { type PixelumeError, type PixelumeSaveMetadata } from '../../core/errors.js';
3
+ import type { PixelumeRequestOptions } from '../../core/types.js';
4
+ import type { SessionManager } from '../auth/SessionManager.js';
5
+ /** A cloud save with its data. `data` is whatever the game stored: validate it before use. */
6
+ export interface PixelumeSave<T extends object = Record<string, unknown>> extends PixelumeSaveMetadata {
7
+ readonly data: T;
8
+ }
9
+ export interface PixelumeSaveList {
10
+ readonly gameId: string;
11
+ /** Metadata of every live slot of the current player in this game (no data). */
12
+ readonly saves: readonly PixelumeSaveMetadata[];
13
+ /** Limits currently enforced by the API. */
14
+ readonly limits: {
15
+ readonly maxSlotsPerGame: number;
16
+ readonly maxDataBytes: number;
17
+ /** Sum of all games of the player. */
18
+ readonly maxBytesPerPlayer: number;
19
+ };
20
+ readonly usage: {
21
+ readonly slots: number;
22
+ readonly playerBytes: number;
23
+ };
24
+ }
25
+ export interface PixelumeSaveWrite<T extends object> {
26
+ /** Version of the game's save format (integer 1–1 000 000), used by the game to migrate. */
27
+ readonly schemaVersion: number;
28
+ /**
29
+ * Revision the game last saw for this slot: `0` creates a new save (fails if one exists);
30
+ * `n` updates only if the slot is still at revision `n`. There is no blind overwrite.
31
+ */
32
+ readonly expectedRevision: number;
33
+ /** A JSON object (plain objects, arrays, strings, finite numbers, booleans, null). */
34
+ readonly data: T;
35
+ }
36
+ export interface PixelumeSaveDeleteOptions extends PixelumeRequestOptions {
37
+ /** Current revision of the save to delete (≥ 1). */
38
+ readonly expectedRevision: number;
39
+ }
40
+ export interface PixelumeSaveResult {
41
+ /** `true` when the slot was created (201), `false` when an existing save was updated (200). */
42
+ readonly created: boolean;
43
+ readonly save: PixelumeSaveMetadata;
44
+ }
45
+ /**
46
+ * `deleted`: the save at `expectedRevision` is gone (repeating the same delete also returns
47
+ * `deleted`). `not_found`: there was no save in this slot.
48
+ */
49
+ export type PixelumeSaveDeleteResult = 'deleted' | 'not_found';
50
+ export type PixelumeSavesOperation = 'list' | 'load' | 'save' | 'delete';
51
+ /** Outcome of the last explicit Saves call, for status UIs. Never contains save data. */
52
+ export interface PixelumeSavesActivity {
53
+ /** Saves calls currently in flight. */
54
+ readonly pending: number;
55
+ /** `save`/`delete` calls currently in flight. */
56
+ readonly pendingWrites: number;
57
+ readonly last: {
58
+ readonly operation: PixelumeSavesOperation;
59
+ readonly slotId: string | null;
60
+ readonly outcome: 'success' | 'conflict' | 'error';
61
+ /** Local time (ms since epoch) when the call settled. */
62
+ readonly at: number;
63
+ readonly error: PixelumeError | null;
64
+ } | null;
65
+ }
66
+ export type PixelumeSavesListener = (activity: PixelumeSavesActivity, previous: PixelumeSavesActivity) => void;
67
+ /**
68
+ * Cloud Saves of the signed-in player for the configured `gameId`. Every call needs a session
69
+ * with the `saves` scope; the API derives the player from the access token, never from input.
70
+ *
71
+ * Saves are unverified, client-supplied personal data: never use them as proof of achievements,
72
+ * rankings or rewards, and never store secrets or personal data in them.
73
+ */
74
+ export interface PixelumeSavesApi {
75
+ /** Lists slot metadata, limits and usage. */
76
+ list(options?: PixelumeRequestOptions): Promise<PixelumeSaveList>;
77
+ /** Loads one slot, or `null` when it has no save. */
78
+ load<T extends object = Record<string, unknown>>(slotId: string, options?: PixelumeRequestOptions): Promise<PixelumeSave<T> | null>;
79
+ /**
80
+ * Creates (`expectedRevision: 0`) or updates a slot. A revision mismatch rejects with
81
+ * `PixelumeSaveConflictError`; the SDK never retries it or overwrites the remote save.
82
+ */
83
+ save<T extends object>(slotId: string, write: PixelumeSaveWrite<T>, options?: PixelumeRequestOptions): Promise<PixelumeSaveResult>;
84
+ /** Deletes a slot at `expectedRevision`. Conflicts reject with `PixelumeSaveConflictError`. */
85
+ delete(slotId: string, options: PixelumeSaveDeleteOptions): Promise<PixelumeSaveDeleteResult>;
86
+ /** Current activity of explicit Saves calls. */
87
+ readonly activity: PixelumeSavesActivity;
88
+ /** Subscribes to `activity` changes. The returned function removes the listener. */
89
+ subscribe(listener: PixelumeSavesListener): () => void;
90
+ }
91
+ export declare class SavesModule implements PixelumeSavesApi {
92
+ #private;
93
+ constructor(session: SessionManager, config: ResolvedConfig);
94
+ get activity(): PixelumeSavesActivity;
95
+ subscribe(listener: PixelumeSavesListener): () => void;
96
+ list(options?: PixelumeRequestOptions): Promise<PixelumeSaveList>;
97
+ load<T extends object = Record<string, unknown>>(slotId: string, options?: PixelumeRequestOptions): Promise<PixelumeSave<T> | null>;
98
+ save<T extends object>(slotId: string, write: PixelumeSaveWrite<T>, options?: PixelumeRequestOptions): Promise<PixelumeSaveResult>;
99
+ delete(slotId: string, options: PixelumeSaveDeleteOptions): Promise<PixelumeSaveDeleteResult>;
100
+ }