gameglue 1.2.2 → 2.0.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.
@@ -30,17 +30,37 @@
30
30
  color: #64748b;
31
31
  margin-bottom: 2rem;
32
32
  }
33
+ .header {
34
+ display: flex;
35
+ justify-content: space-between;
36
+ align-items: center;
37
+ margin-bottom: 1.5rem;
38
+ }
33
39
  .status {
34
40
  display: inline-block;
35
41
  padding: 0.25rem 0.75rem;
36
42
  border-radius: 9999px;
37
43
  font-size: 0.875rem;
38
- margin-bottom: 1.5rem;
39
44
  }
40
45
  .status.disconnected { background: #7f1d1d; color: #fca5a5; }
41
46
  .status.connecting { background: #78350f; color: #fcd34d; }
42
47
  .status.connected { background: #14532d; color: #86efac; }
43
48
 
49
+ .logout-btn {
50
+ background: transparent;
51
+ border: 1px solid #334155;
52
+ color: #94a3b8;
53
+ padding: 0.5rem 1rem;
54
+ border-radius: 0.5rem;
55
+ font-size: 0.875rem;
56
+ cursor: pointer;
57
+ transition: all 0.15s;
58
+ }
59
+ .logout-btn:hover {
60
+ background: #334155;
61
+ color: #f1f5f9;
62
+ }
63
+
44
64
  .card {
45
65
  background: #1e293b;
46
66
  border-radius: 0.75rem;
@@ -164,12 +184,15 @@
164
184
  <!-- Auth Section (shown when not authenticated) -->
165
185
  <div id="auth-section" class="card auth-section">
166
186
  <p style="margin-bottom: 1rem; color: #94a3b8;">Connect to GameGlue to view flight telemetry and send commands</p>
167
- <button class="auth-btn" onclick="authenticate()">Connect with GameGlue</button>
187
+ <button class="auth-btn" onclick="doLogin()">Connect with GameGlue</button>
168
188
  </div>
169
189
 
170
190
  <!-- Dashboard (shown when authenticated) -->
171
191
  <div id="dashboard" class="hidden">
172
- <span id="status" class="status disconnected">Waiting for MSFS...</span>
192
+ <div class="header">
193
+ <span id="status" class="status disconnected">Waiting for MSFS...</span>
194
+ <button class="logout-btn" onclick="doLogout()">Logout</button>
195
+ </div>
173
196
 
174
197
  <!-- Telemetry Card -->
175
198
  <div class="card">
@@ -259,47 +282,55 @@
259
282
  </div>
260
283
 
261
284
  <!-- Load GameGlue SDK from local build -->
262
- <script src="../dist/gg.sdk.js"></script>
285
+ <script src="../dist/gg.umd.js"></script>
263
286
  <script>
264
287
  // Configuration
265
288
  const CLIENT_ID = 'gameglue-sdk-examples';
266
- const REDIRECT_URI = window.location.href.split('?')[0];
289
+ const REDIRECT_URI = window.location.href.split('?')[0].split('#')[0];
290
+
291
+ // Create a single SDK instance
292
+ const ggClient = new GameGlue({
293
+ clientId: CLIENT_ID,
294
+ redirect_uri: REDIRECT_URI,
295
+ scopes: ['msfs:read', 'msfs:write']
296
+ });
267
297
 
268
- let ggClient = null;
269
298
  let listener = null;
270
299
 
271
- // Initialize GameGlue client
272
- function initGameGlue() {
273
- ggClient = new GameGlue({
274
- clientId: CLIENT_ID,
275
- redirect_uri: REDIRECT_URI,
276
- scopes: ['msfs:read', 'msfs:write']
277
- });
300
+ // Login button handler
301
+ function doLogin() {
302
+ log('Redirecting to GameGlue login...');
303
+ ggClient.login();
304
+ }
305
+
306
+ // Logout button handler
307
+ function doLogout() {
308
+ log('Logging out...');
309
+ ggClient.logout({ redirect: false });
310
+ document.getElementById('auth-section').classList.remove('hidden');
311
+ document.getElementById('dashboard').classList.add('hidden');
278
312
  }
279
313
 
280
- // Authenticate with GameGlue
281
- async function authenticate() {
314
+ // Check authentication and set up listener
315
+ async function init() {
282
316
  try {
283
- log('Authenticating with GameGlue...');
284
- const userId = await ggClient.auth();
317
+ log('Checking authentication...');
285
318
 
286
- if (userId) {
287
- log('Authenticated! User ID: ' + userId);
288
- showDashboard();
289
- await setupListener(userId);
319
+ // isAuthenticated() handles OAuth callback automatically
320
+ const isAuthed = await ggClient.isAuthenticated();
321
+
322
+ if (!isAuthed) {
323
+ log('Not authenticated - please log in');
324
+ return;
290
325
  }
291
- } catch (err) {
292
- log('Authentication failed: ' + err.message, 'error');
293
- }
294
- }
295
326
 
296
- // Check if already authenticated on page load
297
- async function checkAuth() {
298
- if (await ggClient.isAuthenticated()) {
299
- const userId = ggClient.getUserId();
300
- log('Already authenticated. User ID: ' + userId);
327
+ // Authenticated
328
+ const userId = ggClient.getUser();
329
+ log('Authenticated! User ID: ' + userId);
301
330
  showDashboard();
302
331
  await setupListener(userId);
332
+ } catch (err) {
333
+ log('Error: ' + err.message, 'error');
303
334
  }
304
335
  }
305
336
 
@@ -383,19 +414,18 @@
383
414
 
384
415
  function log(message, type = '') {
385
416
  const logEl = document.getElementById('log');
386
- const entry = document.createElement('div');
387
- entry.className = 'log-entry ' + type;
388
- entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
389
- logEl.appendChild(entry);
390
- logEl.scrollTop = logEl.scrollHeight;
417
+ if (logEl) {
418
+ const entry = document.createElement('div');
419
+ entry.className = 'log-entry ' + type;
420
+ entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
421
+ logEl.appendChild(entry);
422
+ logEl.scrollTop = logEl.scrollHeight;
423
+ }
391
424
  console.log(message);
392
425
  }
393
426
 
394
427
  // Initialize on page load
395
- window.onload = () => {
396
- initGameGlue();
397
- checkAuth();
398
- };
428
+ window.onload = init;
399
429
  </script>
400
430
  </body>
401
431
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gameglue",
3
- "version": "1.2.2",
3
+ "version": "2.0.0",
4
4
  "description": "Javascript SDK for the GameGlue developer platform.",
5
5
  "type": "module",
6
6
  "main": "dist/gg.cjs.js",
package/src/auth.js CHANGED
@@ -2,107 +2,224 @@ import { OidcClient } from 'oidc-client-ts';
2
2
  import { storage } from './utils';
3
3
  import jwt_decode from 'jwt-decode';
4
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;
5
9
 
6
10
  export class GameGlueAuth {
7
11
  constructor(cfg) {
12
+ const authority = cfg.authUrl || DEFAULT_AUTH_URL;
8
13
  this._oidcSettings = {
9
- authority: "https://auth.gameglue.gg/realms/GameGlue",
14
+ authority,
10
15
  client_id: cfg.clientId,
11
16
  redirect_uri: removeTrailingSlashes(cfg.redirect_uri || window.location.href),
12
17
  post_logout_redirect_uri: removeTrailingSlashes(window.location.href),
13
18
  response_type: "code",
14
- scope: `openid ${(cfg.scopes||[]).join(' ')}`,
19
+ scope: `openid ${(cfg.scopes || []).join(' ')}`,
15
20
  response_mode: "fragment",
16
21
  filterProtocolClaims: true
17
22
  };
18
23
  this._oidcClient = new OidcClient(this._oidcSettings);
19
- this._refreshCallback = () => {}
24
+ this._refreshCallback = () => {};
20
25
  this._refreshTimeout = null;
21
26
  }
22
- setTokenRefreshTimeout(token) {
23
- if (!token) {
24
- return;
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();
25
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
+ console.error('Failed to create signin request:', err);
53
+ });
54
+ }
55
+
56
+ /**
57
+ * Log out the user.
58
+ * Clears local tokens and optionally redirects to Keycloak logout.
59
+ * @param {Object} options - { redirect?: boolean }
60
+ */
61
+ logout(options = {}) {
62
+ // Clear local tokens
63
+ storage.remove('gg-auth-token');
64
+ storage.remove('gg-refresh-token');
26
65
  clearTimeout(this._refreshTimeout);
27
- const timeUntilExp = (jwt_decode(token).exp * 1000) - Date.now() - 5000;
28
- if (timeUntilExp > 0) {
29
- this._refreshTimeout = setTimeout(() => {
30
- this.attemptRefresh();
31
- }, timeUntilExp);
66
+
67
+ // Optionally redirect to Keycloak logout
68
+ if (options.redirect !== false) {
69
+ const logoutUrl = `${this._oidcSettings.authority}/protocol/openid-connect/logout?post_logout_redirect_uri=${encodeURIComponent(this._oidcSettings.post_logout_redirect_uri)}`;
70
+ window.location.href = logoutUrl;
32
71
  }
33
72
  }
34
- setAccessToken(token) {
35
- this.setTokenRefreshTimeout(token);
36
- return storage.set('gg-auth-token', token);
73
+
74
+ /**
75
+ * Get the current user's ID.
76
+ * @throws {Error} if not authenticated
77
+ * @returns {string}
78
+ */
79
+ getUser() {
80
+ const token = this._getAccessToken();
81
+ if (!token) {
82
+ throw new Error('Not authenticated');
83
+ }
84
+ const decoded = jwt_decode(token);
85
+ return decoded.sub;
37
86
  }
87
+
88
+ /**
89
+ * Get the access token for API calls.
90
+ * @returns {string|null}
91
+ */
38
92
  getAccessToken() {
39
- let token = storage.get('gg-auth-token');
40
- this.setTokenRefreshTimeout(token);
41
- return token;
93
+ return this._getAccessToken();
42
94
  }
43
- getUserId() {
44
- const decoded = jwt_decode(this.getAccessToken());
45
- return decoded.sub;
46
- }
47
- setRefreshToken(token) {
48
- return storage.set('gg-refresh-token', token);
95
+
96
+ /**
97
+ * Register callback for token refresh events.
98
+ * @param {Function} callback
99
+ */
100
+ onTokenRefreshed(callback) {
101
+ this._refreshCallback = callback;
49
102
  }
50
- getRefreshToken(token) {
51
- return storage.get('gg-refresh-token');
103
+
104
+ // ============ Internal Methods ============
105
+
106
+ _hasCallbackParams() {
107
+ return location.hash.includes("state=") &&
108
+ (location.hash.includes("code=") || location.hash.includes("error="));
52
109
  }
53
- _shouldHandleRedirectResponse() {
54
- return (location.hash.includes("state=") && (location.hash.includes("code=") || location.hash.includes("error=")));
110
+
111
+ _clearCallbackUrl() {
112
+ window.history.replaceState("", document.title, window.location.pathname + window.location.search);
55
113
  }
56
- async handleRedirectResponse() {
57
- let response = await this._oidcClient.processSigninResponse(window.location.href);
58
- if (response.error || !response.access_token) {
59
- console.error(response.error);
114
+
115
+ async _processCallback() {
116
+ // If already processing, wait for that to complete
117
+ if (_callbackPromise) {
118
+ await _callbackPromise;
60
119
  return;
61
120
  }
62
- window.history.pushState("", document.title, window.location.pathname + window.location.search);
63
- this.setAccessToken(response.access_token);
64
- this.setRefreshToken(response.refresh_token);
121
+
122
+ // Start processing
123
+ _callbackPromise = this._doProcessCallback();
124
+
125
+ try {
126
+ await _callbackPromise;
127
+ } finally {
128
+ _callbackPromise = null;
129
+ }
65
130
  }
66
- onTokenRefreshed(callback) {
67
- this._refreshCallback = callback;
131
+
132
+ async _doProcessCallback() {
133
+ try {
134
+ const response = await this._oidcClient.processSigninResponse(window.location.href);
135
+
136
+ if (response.error) {
137
+ this._clearCallbackUrl();
138
+ throw new Error(response.error);
139
+ }
140
+
141
+ if (!response.access_token) {
142
+ this._clearCallbackUrl();
143
+ throw new Error('No access token received');
144
+ }
145
+
146
+ this._setAccessToken(response.access_token);
147
+ this._setRefreshToken(response.refresh_token);
148
+ this._clearCallbackUrl();
149
+ } catch (err) {
150
+ // If we failed but tokens exist (another call succeeded), that's fine
151
+ if (this._hasValidTokens()) {
152
+ this._clearCallbackUrl();
153
+ return;
154
+ }
155
+ this._clearCallbackUrl();
156
+ throw err;
157
+ }
68
158
  }
69
- async isAuthenticated(refreshAttempted) {
70
- // 1. Get the access token
71
- let access_token = this.getAccessToken();
72
-
73
- // 2. If we don't have an access token, we're not authenticated
74
- if (!access_token) {
159
+
160
+ _hasValidTokens() {
161
+ const token = this._getAccessToken();
162
+ if (!token) {
75
163
  return false;
76
164
  }
77
- // 3. Decode the token, then check to see if it has expired
78
- const decoded = jwt_decode(access_token);
79
- const expirationDate = new Date(decoded.exp*1000);
80
- const isExpired = (expirationDate < new Date());
81
-
82
- if (isExpired && !refreshAttempted) {
83
- await this.attemptRefresh();
84
- return this.isAuthenticated(true);
165
+
166
+ try {
167
+ const decoded = jwt_decode(token);
168
+ const expirationDate = new Date(decoded.exp * 1000);
169
+ return expirationDate > new Date();
170
+ } catch {
171
+ return false;
85
172
  }
86
-
87
- // This line might be a little confusing. Basically it's just saying if we tried to refresh the token,
88
- // but it's STILL expired, return false, otherwise return true.
89
- return !(isExpired && refreshAttempted);
90
173
  }
91
- isTokenExpired(token) {
92
- const decoded = jwt_decode(token);
93
- const expirationDate = new Date(decoded.exp*1000);
94
- return (expirationDate < new Date());
174
+
175
+ _getAccessToken() {
176
+ const token = storage.get('gg-auth-token');
177
+ if (token) {
178
+ this._setTokenRefreshTimeout(token);
179
+ }
180
+ return token;
181
+ }
182
+
183
+ _setAccessToken(token) {
184
+ this._setTokenRefreshTimeout(token);
185
+ return storage.set('gg-auth-token', token);
95
186
  }
96
- async attemptRefresh() {
187
+
188
+ _setRefreshToken(token) {
189
+ return storage.set('gg-refresh-token', token);
190
+ }
191
+
192
+ _getRefreshToken() {
193
+ return storage.get('gg-refresh-token');
194
+ }
195
+
196
+ _setTokenRefreshTimeout(token) {
197
+ if (!token) return;
198
+
199
+ clearTimeout(this._refreshTimeout);
200
+
201
+ try {
202
+ const timeUntilExp = (jwt_decode(token).exp * 1000) - Date.now() - 5000;
203
+ if (timeUntilExp > 0) {
204
+ this._refreshTimeout = setTimeout(() => {
205
+ this._attemptRefresh();
206
+ }, timeUntilExp);
207
+ }
208
+ } catch {
209
+ // Invalid token, ignore
210
+ }
211
+ }
212
+
213
+ async _attemptRefresh() {
97
214
  const url = `${this._oidcSettings.authority}/protocol/openid-connect/token`;
98
215
  const client_id = this._oidcSettings.client_id;
99
- const refresh_token = this.getRefreshToken();
216
+ const refresh_token = this._getRefreshToken();
100
217
  const grant_type = 'refresh_token';
101
-
218
+
102
219
  try {
103
- const response = await fetch(url, {
220
+ const response = await fetch(url, {
104
221
  method: 'POST',
105
- headers:{
222
+ headers: {
106
223
  'Content-Type': 'application/x-www-form-urlencoded'
107
224
  },
108
225
  body: new URLSearchParams({
@@ -111,31 +228,15 @@ export class GameGlueAuth {
111
228
  refresh_token
112
229
  })
113
230
  });
231
+
114
232
  if (response.status === 200) {
115
233
  const resObj = await response.json();
116
- this.setAccessToken(resObj.access_token);
117
- this.setRefreshToken(resObj.refresh_token);
118
- this._refreshCallback(resObj);
234
+ this._setAccessToken(resObj.access_token);
235
+ this._setRefreshToken(resObj.refresh_token);
236
+ this._refreshCallback(resObj.access_token);
119
237
  }
120
- } catch(e) {
121
- console.log('Error: ', e);
122
- }
123
- }
124
- _triggerAuthRedirect() {
125
- this._oidcClient.createSigninRequest({ state: { bar: 15 } }).then(function(req) {
126
- window.location = req.url;
127
- }).catch(function(err) {
128
- console.error(err);
129
- });
130
- }
131
- async authenticate() {
132
- if (this._shouldHandleRedirectResponse()) {
133
- await this.handleRedirectResponse();
134
- }
135
-
136
- let isAuthenticated = await this.isAuthenticated();
137
- if (!isAuthenticated) {
138
- await this._triggerAuthRedirect();
238
+ } catch (e) {
239
+ console.error('Token refresh failed:', e);
139
240
  }
140
241
  }
141
242
  }
@@ -145,4 +246,4 @@ function removeTrailingSlashes(url) {
145
246
  return url.replace(/\/+$/, '');
146
247
  }
147
248
  return url;
148
- }
249
+ }