gameglue 4.0.1 → 4.0.2

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.
Files changed (56) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +275 -275
  3. package/babel.config.cjs +5 -5
  4. package/coverage/auth.js.html +525 -525
  5. package/coverage/base.css +224 -224
  6. package/coverage/block-navigation.js +87 -87
  7. package/coverage/favicon.png +0 -0
  8. package/coverage/index.html +175 -175
  9. package/coverage/index.js.html +309 -309
  10. package/coverage/lcov-report/auth.js.html +525 -525
  11. package/coverage/lcov-report/base.css +224 -224
  12. package/coverage/lcov-report/block-navigation.js +87 -87
  13. package/coverage/lcov-report/favicon.png +0 -0
  14. package/coverage/lcov-report/index.html +175 -175
  15. package/coverage/lcov-report/index.js.html +309 -309
  16. package/coverage/lcov-report/listener.js.html +528 -528
  17. package/coverage/lcov-report/prettify.css +1 -1
  18. package/coverage/lcov-report/prettify.js +2 -2
  19. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  20. package/coverage/lcov-report/sorter.js +210 -210
  21. package/coverage/lcov-report/user.js.html +117 -117
  22. package/coverage/lcov-report/utils.js.html +117 -117
  23. package/coverage/lcov.info +391 -391
  24. package/coverage/listener.js.html +528 -528
  25. package/coverage/prettify.css +1 -1
  26. package/coverage/prettify.js +2 -2
  27. package/coverage/sort-arrow-sprite.png +0 -0
  28. package/coverage/sorter.js +210 -210
  29. package/coverage/user.js.html +117 -117
  30. package/coverage/utils.js.html +117 -117
  31. package/dist/gg.cjs.js +1 -1
  32. package/dist/gg.cjs.js.map +1 -1
  33. package/dist/gg.esm.js +1 -1
  34. package/dist/gg.esm.js.map +1 -1
  35. package/dist/gg.umd.js +1 -1
  36. package/dist/gg.umd.js.map +1 -1
  37. package/examples/certs/cert.pem +19 -19
  38. package/examples/certs/key.pem +28 -28
  39. package/examples/flight-dashboard.html +431 -431
  40. package/examples/server.js +99 -99
  41. package/examples/telemetry-validator.html +1410 -1410
  42. package/jest.config.cjs +33 -33
  43. package/package.json +56 -56
  44. package/rollup.config.js +57 -57
  45. package/src/auth.js +255 -255
  46. package/src/auth.spec.js +481 -481
  47. package/src/index.js +168 -168
  48. package/src/listener.js +196 -196
  49. package/src/listener.spec.js +598 -598
  50. package/src/presence_listener.js +112 -112
  51. package/src/test/fixtures.js +106 -106
  52. package/src/test/setup.js +51 -51
  53. package/src/utils.js +63 -63
  54. package/src/utils.spec.js +78 -78
  55. package/types/index.d.ts +338 -338
  56. package/webpack.config.js +15 -15
package/src/auth.js CHANGED
@@ -1,255 +1,255 @@
1
- import { OidcClient } from 'oidc-client-ts';
2
- import { storage, isCorsError, logCorsHelp } from './utils';
3
- import jwt_decode from 'jwt-decode';
4
-
5
- const DEFAULT_AUTH_URL = 'https://auth.gameglue.gg/realms/GameGlue';
6
-
7
- // Track if callback is being processed (prevents double-processing)
8
- let _callbackPromise = null;
9
-
10
- export class GameGlueAuth {
11
- constructor(cfg) {
12
- const authority = cfg.authUrl || DEFAULT_AUTH_URL;
13
- this._oidcSettings = {
14
- authority,
15
- client_id: cfg.clientId,
16
- redirect_uri: removeTrailingSlashes(cfg.redirect_uri || window.location.href),
17
- post_logout_redirect_uri: removeTrailingSlashes(window.location.href),
18
- response_type: "code",
19
- scope: `openid ${(cfg.scopes || []).join(' ')}`,
20
- response_mode: "fragment",
21
- filterProtocolClaims: true
22
- };
23
- this._oidcClient = new OidcClient(this._oidcSettings);
24
- this._refreshCallback = () => {};
25
- this._refreshTimeout = null;
26
- }
27
-
28
- /**
29
- * Check if user is authenticated.
30
- * If OAuth callback params are in URL, processes them first.
31
- * Safe to call multiple times - idempotent.
32
- * @returns {Promise<boolean>}
33
- */
34
- async isAuthenticated() {
35
- // If callback params present, process them first
36
- if (this._hasCallbackParams()) {
37
- await this._processCallback();
38
- }
39
-
40
- // Check for valid tokens
41
- return this._hasValidTokens();
42
- }
43
-
44
- /**
45
- * Redirect to OAuth login page.
46
- * Does not return - navigates away.
47
- */
48
- login() {
49
- this._oidcClient.createSigninRequest({ state: { bar: 15 } }).then((req) => {
50
- window.location = req.url;
51
- }).catch((err) => {
52
- if (isCorsError(err)) {
53
- logCorsHelp('Login Request', this._oidcSettings.authority);
54
- }
55
- console.error('Failed to create signin request:', err);
56
- });
57
- }
58
-
59
- /**
60
- * Log out the user.
61
- * Clears local tokens and optionally redirects to Keycloak logout.
62
- * @param {Object} options - { redirect?: boolean }
63
- */
64
- logout(options = {}) {
65
- // Clear local tokens
66
- storage.remove('gg-auth-token');
67
- storage.remove('gg-refresh-token');
68
- clearTimeout(this._refreshTimeout);
69
-
70
- // Optionally redirect to Keycloak logout
71
- if (options.redirect !== false) {
72
- const logoutUrl = `${this._oidcSettings.authority}/protocol/openid-connect/logout?post_logout_redirect_uri=${encodeURIComponent(this._oidcSettings.post_logout_redirect_uri)}`;
73
- window.location.href = logoutUrl;
74
- }
75
- }
76
-
77
- /**
78
- * Get the current user's ID.
79
- * @throws {Error} if not authenticated
80
- * @returns {string}
81
- */
82
- getUser() {
83
- const token = this._getAccessToken();
84
- if (!token) {
85
- throw new Error('Not authenticated');
86
- }
87
- const decoded = jwt_decode(token);
88
- return decoded.sub;
89
- }
90
-
91
- /**
92
- * Get the access token for API calls.
93
- * @returns {string|null}
94
- */
95
- getAccessToken() {
96
- return this._getAccessToken();
97
- }
98
-
99
- /**
100
- * Register callback for token refresh events.
101
- * @param {Function} callback
102
- */
103
- onTokenRefreshed(callback) {
104
- this._refreshCallback = callback;
105
- }
106
-
107
- // ============ Internal Methods ============
108
-
109
- _hasCallbackParams() {
110
- return location.hash.includes("state=") &&
111
- (location.hash.includes("code=") || location.hash.includes("error="));
112
- }
113
-
114
- _clearCallbackUrl() {
115
- window.history.replaceState("", document.title, window.location.pathname + window.location.search);
116
- }
117
-
118
- async _processCallback() {
119
- // If already processing, wait for that to complete
120
- if (_callbackPromise) {
121
- await _callbackPromise;
122
- return;
123
- }
124
-
125
- // Start processing
126
- _callbackPromise = this._doProcessCallback();
127
-
128
- try {
129
- await _callbackPromise;
130
- } finally {
131
- _callbackPromise = null;
132
- }
133
- }
134
-
135
- async _doProcessCallback() {
136
- try {
137
- const response = await this._oidcClient.processSigninResponse(window.location.href);
138
-
139
- if (response.error) {
140
- this._clearCallbackUrl();
141
- throw new Error(response.error);
142
- }
143
-
144
- if (!response.access_token) {
145
- this._clearCallbackUrl();
146
- throw new Error('No access token received');
147
- }
148
-
149
- this._setAccessToken(response.access_token);
150
- this._setRefreshToken(response.refresh_token);
151
- this._clearCallbackUrl();
152
- } catch (err) {
153
- // If we failed but tokens exist (another call succeeded), that's fine
154
- if (this._hasValidTokens()) {
155
- this._clearCallbackUrl();
156
- return;
157
- }
158
- this._clearCallbackUrl();
159
- throw err;
160
- }
161
- }
162
-
163
- _hasValidTokens() {
164
- const token = this._getAccessToken();
165
- if (!token) {
166
- return false;
167
- }
168
-
169
- try {
170
- const decoded = jwt_decode(token);
171
- const expirationDate = new Date(decoded.exp * 1000);
172
- return expirationDate > new Date();
173
- } catch {
174
- return false;
175
- }
176
- }
177
-
178
- _getAccessToken() {
179
- const token = storage.get('gg-auth-token');
180
- if (token) {
181
- this._setTokenRefreshTimeout(token);
182
- }
183
- return token || undefined;
184
- }
185
-
186
- _setAccessToken(token) {
187
- this._setTokenRefreshTimeout(token);
188
- return storage.set('gg-auth-token', token);
189
- }
190
-
191
- _setRefreshToken(token) {
192
- return storage.set('gg-refresh-token', token);
193
- }
194
-
195
- _getRefreshToken() {
196
- return storage.get('gg-refresh-token');
197
- }
198
-
199
- _setTokenRefreshTimeout(token) {
200
- if (!token) return;
201
-
202
- clearTimeout(this._refreshTimeout);
203
-
204
- try {
205
- const timeUntilExp = (jwt_decode(token).exp * 1000) - Date.now() - 5000;
206
- if (timeUntilExp > 0) {
207
- this._refreshTimeout = setTimeout(() => {
208
- this._attemptRefresh();
209
- }, timeUntilExp);
210
- }
211
- } catch {
212
- // Invalid token, ignore
213
- }
214
- }
215
-
216
- async _attemptRefresh() {
217
- const url = `${this._oidcSettings.authority}/protocol/openid-connect/token`;
218
- const client_id = this._oidcSettings.client_id;
219
- const refresh_token = this._getRefreshToken();
220
- const grant_type = 'refresh_token';
221
-
222
- try {
223
- const response = await fetch(url, {
224
- method: 'POST',
225
- headers: {
226
- 'Content-Type': 'application/x-www-form-urlencoded'
227
- },
228
- body: new URLSearchParams({
229
- client_id,
230
- grant_type,
231
- refresh_token
232
- })
233
- });
234
-
235
- if (response.status === 200) {
236
- const resObj = await response.json();
237
- this._setAccessToken(resObj.access_token);
238
- this._setRefreshToken(resObj.refresh_token);
239
- this._refreshCallback(resObj.access_token);
240
- }
241
- } catch (e) {
242
- if (isCorsError(e)) {
243
- logCorsHelp('Token Refresh', url);
244
- }
245
- console.error('Token refresh failed:', e);
246
- }
247
- }
248
- }
249
-
250
- function removeTrailingSlashes(url) {
251
- if (url.endsWith('/')) {
252
- return url.replace(/\/+$/, '');
253
- }
254
- return url;
255
- }
1
+ import { OidcClient } from 'oidc-client-ts';
2
+ import { storage, isCorsError, logCorsHelp } from './utils';
3
+ import jwt_decode from 'jwt-decode';
4
+
5
+ const DEFAULT_AUTH_URL = 'https://auth.gameglue.gg/realms/GameGlue';
6
+
7
+ // Track if callback is being processed (prevents double-processing)
8
+ let _callbackPromise = null;
9
+
10
+ export class GameGlueAuth {
11
+ constructor(cfg) {
12
+ const authority = cfg.authUrl || DEFAULT_AUTH_URL;
13
+ this._oidcSettings = {
14
+ authority,
15
+ client_id: cfg.clientId,
16
+ redirect_uri: removeTrailingSlashes(cfg.redirect_uri || window.location.href),
17
+ post_logout_redirect_uri: removeTrailingSlashes(window.location.href),
18
+ response_type: "code",
19
+ scope: `openid ${(cfg.scopes || []).join(' ')}`,
20
+ response_mode: "fragment",
21
+ filterProtocolClaims: true
22
+ };
23
+ this._oidcClient = new OidcClient(this._oidcSettings);
24
+ this._refreshCallback = () => {};
25
+ this._refreshTimeout = null;
26
+ }
27
+
28
+ /**
29
+ * Check if user is authenticated.
30
+ * If OAuth callback params are in URL, processes them first.
31
+ * Safe to call multiple times - idempotent.
32
+ * @returns {Promise<boolean>}
33
+ */
34
+ async isAuthenticated() {
35
+ // If callback params present, process them first
36
+ if (this._hasCallbackParams()) {
37
+ await this._processCallback();
38
+ }
39
+
40
+ // Check for valid tokens
41
+ return this._hasValidTokens();
42
+ }
43
+
44
+ /**
45
+ * Redirect to OAuth login page.
46
+ * Does not return - navigates away.
47
+ */
48
+ login() {
49
+ this._oidcClient.createSigninRequest({ state: { bar: 15 } }).then((req) => {
50
+ window.location = req.url;
51
+ }).catch((err) => {
52
+ if (isCorsError(err)) {
53
+ logCorsHelp('Login Request', this._oidcSettings.authority);
54
+ }
55
+ console.error('Failed to create signin request:', err);
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Log out the user.
61
+ * Clears local tokens and optionally redirects to Keycloak logout.
62
+ * @param {Object} options - { redirect?: boolean }
63
+ */
64
+ logout(options = {}) {
65
+ // Clear local tokens
66
+ storage.remove('gg-auth-token');
67
+ storage.remove('gg-refresh-token');
68
+ clearTimeout(this._refreshTimeout);
69
+
70
+ // Optionally redirect to Keycloak logout
71
+ if (options.redirect !== false) {
72
+ const logoutUrl = `${this._oidcSettings.authority}/protocol/openid-connect/logout?post_logout_redirect_uri=${encodeURIComponent(this._oidcSettings.post_logout_redirect_uri)}`;
73
+ window.location.href = logoutUrl;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Get the current user's ID.
79
+ * @throws {Error} if not authenticated
80
+ * @returns {string}
81
+ */
82
+ getUser() {
83
+ const token = this._getAccessToken();
84
+ if (!token) {
85
+ throw new Error('Not authenticated');
86
+ }
87
+ const decoded = jwt_decode(token);
88
+ return decoded.sub;
89
+ }
90
+
91
+ /**
92
+ * Get the access token for API calls.
93
+ * @returns {string|null}
94
+ */
95
+ getAccessToken() {
96
+ return this._getAccessToken();
97
+ }
98
+
99
+ /**
100
+ * Register callback for token refresh events.
101
+ * @param {Function} callback
102
+ */
103
+ onTokenRefreshed(callback) {
104
+ this._refreshCallback = callback;
105
+ }
106
+
107
+ // ============ Internal Methods ============
108
+
109
+ _hasCallbackParams() {
110
+ return location.hash.includes("state=") &&
111
+ (location.hash.includes("code=") || location.hash.includes("error="));
112
+ }
113
+
114
+ _clearCallbackUrl() {
115
+ window.history.replaceState("", document.title, window.location.pathname + window.location.search);
116
+ }
117
+
118
+ async _processCallback() {
119
+ // If already processing, wait for that to complete
120
+ if (_callbackPromise) {
121
+ await _callbackPromise;
122
+ return;
123
+ }
124
+
125
+ // Start processing
126
+ _callbackPromise = this._doProcessCallback();
127
+
128
+ try {
129
+ await _callbackPromise;
130
+ } finally {
131
+ _callbackPromise = null;
132
+ }
133
+ }
134
+
135
+ async _doProcessCallback() {
136
+ try {
137
+ const response = await this._oidcClient.processSigninResponse(window.location.href);
138
+
139
+ if (response.error) {
140
+ this._clearCallbackUrl();
141
+ throw new Error(response.error);
142
+ }
143
+
144
+ if (!response.access_token) {
145
+ this._clearCallbackUrl();
146
+ throw new Error('No access token received');
147
+ }
148
+
149
+ this._setAccessToken(response.access_token);
150
+ this._setRefreshToken(response.refresh_token);
151
+ this._clearCallbackUrl();
152
+ } catch (err) {
153
+ // If we failed but tokens exist (another call succeeded), that's fine
154
+ if (this._hasValidTokens()) {
155
+ this._clearCallbackUrl();
156
+ return;
157
+ }
158
+ this._clearCallbackUrl();
159
+ throw err;
160
+ }
161
+ }
162
+
163
+ _hasValidTokens() {
164
+ const token = this._getAccessToken();
165
+ if (!token) {
166
+ return false;
167
+ }
168
+
169
+ try {
170
+ const decoded = jwt_decode(token);
171
+ const expirationDate = new Date(decoded.exp * 1000);
172
+ return expirationDate > new Date();
173
+ } catch {
174
+ return false;
175
+ }
176
+ }
177
+
178
+ _getAccessToken() {
179
+ const token = storage.get('gg-auth-token');
180
+ if (token) {
181
+ this._setTokenRefreshTimeout(token);
182
+ }
183
+ return token || undefined;
184
+ }
185
+
186
+ _setAccessToken(token) {
187
+ this._setTokenRefreshTimeout(token);
188
+ return storage.set('gg-auth-token', token);
189
+ }
190
+
191
+ _setRefreshToken(token) {
192
+ return storage.set('gg-refresh-token', token);
193
+ }
194
+
195
+ _getRefreshToken() {
196
+ return storage.get('gg-refresh-token');
197
+ }
198
+
199
+ _setTokenRefreshTimeout(token) {
200
+ if (!token) return;
201
+
202
+ clearTimeout(this._refreshTimeout);
203
+
204
+ try {
205
+ const timeUntilExp = (jwt_decode(token).exp * 1000) - Date.now() - 5000;
206
+ if (timeUntilExp > 0) {
207
+ this._refreshTimeout = setTimeout(() => {
208
+ this._attemptRefresh();
209
+ }, timeUntilExp);
210
+ }
211
+ } catch {
212
+ // Invalid token, ignore
213
+ }
214
+ }
215
+
216
+ async _attemptRefresh() {
217
+ const url = `${this._oidcSettings.authority}/protocol/openid-connect/token`;
218
+ const client_id = this._oidcSettings.client_id;
219
+ const refresh_token = this._getRefreshToken();
220
+ const grant_type = 'refresh_token';
221
+
222
+ try {
223
+ const response = await fetch(url, {
224
+ method: 'POST',
225
+ headers: {
226
+ 'Content-Type': 'application/x-www-form-urlencoded'
227
+ },
228
+ body: new URLSearchParams({
229
+ client_id,
230
+ grant_type,
231
+ refresh_token
232
+ })
233
+ });
234
+
235
+ if (response.status === 200) {
236
+ const resObj = await response.json();
237
+ this._setAccessToken(resObj.access_token);
238
+ this._setRefreshToken(resObj.refresh_token);
239
+ this._refreshCallback(resObj.access_token);
240
+ }
241
+ } catch (e) {
242
+ if (isCorsError(e)) {
243
+ logCorsHelp('Token Refresh', url);
244
+ }
245
+ console.error('Token refresh failed:', e);
246
+ }
247
+ }
248
+ }
249
+
250
+ function removeTrailingSlashes(url) {
251
+ if (url.endsWith('/')) {
252
+ return url.replace(/\/+$/, '');
253
+ }
254
+ return url;
255
+ }