gameglue 1.2.4 → 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.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # GameGlueJS (Early Access)
1
+ # GameGlue SDK (Early Access)
2
2
 
3
3
  ## What is GameGlue?
4
4
  GameGlue is a powerful platform that makes it trivial to build addons to your favorite games with nothing more than a little javascript. The SDK supports **bidirectional data flow** - receive telemetry from games and send commands back to control the simulation.
@@ -18,37 +18,45 @@ First, [click here](https://developer.gameglue.gg) to sign up for a GameGlue acc
18
18
  ### Install using one of the following options
19
19
  ##### NPM
20
20
  ```shell
21
- npm i gamegluejs
21
+ npm i gameglue
22
22
  ```
23
23
  ##### Yarn
24
24
  ```shell
25
- yarn add gamegluejs
25
+ yarn add gameglue
26
26
  ```
27
27
  ##### Script tag
28
28
  ```html
29
- <script src="https://unpkg.com/gamegluejs@0.0.10/dist/gg.sdk.js"></script>
29
+ <script src="https://unpkg.com/gameglue/dist/gg.umd.js"></script>
30
30
  ```
31
31
 
32
32
  ### Initialize the SDK
33
33
 
34
34
  ```javascript
35
- const ggClient = new GameGlue({
35
+ const gg = new GameGlue({
36
36
  clientId: '<your-application-id>',
37
37
  redirect_uri: '<your-application-url>',
38
38
  scopes: ['msfs:read', 'msfs:write'] // Include 'write' scope to send commands
39
39
  });
40
40
  ```
41
41
 
42
- ### Authenticate the user
42
+ ### Check authentication and login
43
+
43
44
  ```javascript
44
- await ggClient.auth();
45
- const userId = ggClient.getUserId();
45
+ // Check if authenticated (handles OAuth callback automatically)
46
+ if (!await gg.isAuthenticated()) {
47
+ // Not logged in - show login button or redirect
48
+ gg.login(); // Redirects to GameGlue login
49
+ return;
50
+ }
51
+
52
+ // User is authenticated
53
+ const userId = gg.getUser();
46
54
  ```
47
55
 
48
56
  ### Create a listener
49
57
 
50
58
  ```javascript
51
- const listener = await ggClient.createListener({
59
+ const listener = await gg.createListener({
52
60
  userId: userId,
53
61
  gameId: 'msfs'
54
62
  });
@@ -56,7 +64,7 @@ const listener = await ggClient.createListener({
56
64
 
57
65
  You can optionally subscribe to specific fields only:
58
66
  ```javascript
59
- const listener = await ggClient.createListener({
67
+ const listener = await gg.createListener({
60
68
  userId: userId,
61
69
  gameId: 'msfs',
62
70
  fields: ['indicated_altitude', 'airspeed_indicated', 'heading_indicator']
@@ -93,6 +101,16 @@ if (result.status === 'success') {
93
101
  }
94
102
  ```
95
103
 
104
+ ### Logout
105
+
106
+ ```javascript
107
+ // Clear local tokens only (stays logged into GameGlue)
108
+ gg.logout({ redirect: false });
109
+
110
+ // Or clear tokens and redirect to GameGlue logout
111
+ gg.logout();
112
+ ```
113
+
96
114
  ### Dynamic field subscription
97
115
  You can add or remove fields from your subscription at any time:
98
116
 
@@ -113,10 +131,12 @@ const fields = listener.getFields(); // Returns array or null (all fields)
113
131
 
114
132
  | Method | Description |
115
133
  |--------|-------------|
116
- | `auth()` | Authenticates the user and returns their user ID |
117
- | `getUserId()` | Returns the authenticated user's ID |
118
- | `isAuthenticated()` | Returns true if user is authenticated |
119
- | `createListener(config)` | Creates a listener for game telemetry |
134
+ | `isAuthenticated()` | Check if user is authenticated. Handles OAuth callback automatically if present. Returns `Promise<boolean>` |
135
+ | `login()` | Redirect to GameGlue login page |
136
+ | `logout(options?)` | Log out. Pass `{ redirect: false }` to only clear local tokens |
137
+ | `getUser()` | Returns the authenticated user's ID. Throws if not authenticated |
138
+ | `getAccessToken()` | Returns the current access token |
139
+ | `createListener(config)` | Creates a listener for game telemetry. Connects socket automatically |
120
140
 
121
141
  ### Listener
122
142
 
@@ -128,8 +148,51 @@ const fields = listener.getFields(); // Returns array or null (all fields)
128
148
  | `unsubscribe(fields)` | Remove fields from your subscription |
129
149
  | `getFields()` | Get current subscribed fields (null = all) |
130
150
 
131
- ## Examples
151
+ ## Complete Example
152
+
153
+ ```javascript
154
+ const gg = new GameGlue({
155
+ clientId: 'my-app',
156
+ redirect_uri: window.location.origin,
157
+ scopes: ['msfs:read', 'msfs:write']
158
+ });
132
159
 
133
- See the `examples/` directory for a complete flight dashboard example demonstrating bidirectional data flow
160
+ async function init() {
161
+ // Check auth (handles OAuth callback if returning from login)
162
+ if (!await gg.isAuthenticated()) {
163
+ // Show login UI or auto-redirect
164
+ document.getElementById('login-btn').onclick = () => gg.login();
165
+ return;
166
+ }
167
+
168
+ // Authenticated - set up listener
169
+ const userId = gg.getUser();
170
+ const listener = await gg.createListener({
171
+ userId,
172
+ gameId: 'msfs',
173
+ fields: ['indicated_altitude', 'airspeed_indicated', 'autopilot_master']
174
+ });
175
+
176
+ // Receive telemetry
177
+ listener.on('update', (evt) => {
178
+ console.log('Altitude:', evt.data.indicated_altitude);
179
+ });
180
+
181
+ // Send commands
182
+ document.getElementById('ap-btn').onclick = async () => {
183
+ await listener.sendCommand('AUTOPILOT_ON', true);
184
+ };
185
+
186
+ // Logout
187
+ document.getElementById('logout-btn').onclick = () => {
188
+ gg.logout({ redirect: false });
189
+ location.reload();
190
+ };
191
+ }
192
+
193
+ init();
194
+ ```
134
195
 
196
+ ## Examples
135
197
 
198
+ See the `examples/` directory for a complete flight dashboard example demonstrating bidirectional data flow.
package/dist/gg.cjs.js CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("oidc-client-ts"),t=require("jwt-decode"),s=require("socket.io-client");const i={},n=(e,t)=>o()?localStorage.setItem(e,t):i[e]=t,r=e=>o()?localStorage.getItem(e):i[e],o=()=>!("object"==typeof process&&"[object process]"===String(process));class a{constructor(t){this._oidcSettings={authority:"https://auth.gameglue.gg/realms/GameGlue",client_id:t.clientId,redirect_uri:c(t.redirect_uri||window.location.href),post_logout_redirect_uri:c(window.location.href),response_type:"code",scope:`openid ${(t.scopes||[]).join(" ")}`,response_mode:"fragment",filterProtocolClaims:!0},this._oidcClient=new e.OidcClient(this._oidcSettings),this._refreshCallback=()=>{},this._refreshTimeout=null}setTokenRefreshTimeout(e){if(!e)return;clearTimeout(this._refreshTimeout);const s=1e3*t(e).exp-Date.now()-5e3;s>0&&(this._refreshTimeout=setTimeout(()=>{this.attemptRefresh()},s))}setAccessToken(e){return this.setTokenRefreshTimeout(e),n("gg-auth-token",e)}getAccessToken(){let e=r("gg-auth-token");return this.setTokenRefreshTimeout(e),e}getUserId(){return t(this.getAccessToken()).sub}setRefreshToken(e){return n("gg-refresh-token",e)}getRefreshToken(e){return r("gg-refresh-token")}_shouldHandleRedirectResponse(){return location.hash.includes("state=")&&(location.hash.includes("code=")||location.hash.includes("error="))}async handleRedirectResponse(){let e=await this._oidcClient.processSigninResponse(window.location.href);!e.error&&e.access_token?(window.history.pushState("",document.title,window.location.pathname+window.location.search),this.setAccessToken(e.access_token),this.setRefreshToken(e.refresh_token)):console.error(e.error)}onTokenRefreshed(e){this._refreshCallback=e}async isAuthenticated(e){let s=this.getAccessToken();if(!s)return!1;const i=t(s),n=new Date(1e3*i.exp)<new Date;return n&&!e?(await this.attemptRefresh(),this.isAuthenticated(!0)):!(n&&e)}isTokenExpired(e){const s=t(e);return new Date(1e3*s.exp)<new Date}async attemptRefresh(){const e=`${this._oidcSettings.authority}/protocol/openid-connect/token`,t=this._oidcSettings.client_id,s=this.getRefreshToken();try{const i=await fetch(e,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:t,grant_type:"refresh_token",refresh_token:s})});if(200===i.status){const e=await i.json();this.setAccessToken(e.access_token),this.setRefreshToken(e.refresh_token),this._refreshCallback(e)}}catch(e){console.log("Error: ",e)}}_triggerAuthRedirect(){this._oidcClient.createSigninRequest({state:{bar:15}}).then(function(e){window.location=e.url}).catch(function(e){console.error(e)})}async authenticate(){this._shouldHandleRedirectResponse()&&await this.handleRedirectResponse(),await this.isAuthenticated()||await this._triggerAuthRedirect()}}function c(e){return e.endsWith("/")?e.replace(/\/+$/,""):e}const h=require("event-emitter");class u{constructor(e,t){this._config=t,this._socket=e,this._callbacks=[],this._fields=t.fields?[...t.fields]:null}async establishConnection(){if(!this._socket||!this._config.userId||!this._config.gameId)throw new Error("Missing arguments in establishConnection");return new Promise(e=>{let t;t=this._fields?{userId:this._config.userId,gameId:this._config.gameId,fields:this._fields}:`${this._config.userId}:${this._config.gameId}`,this._socket.timeout(5e3).emit("listen",t,(t,s)=>t?e({status:"failed",reason:"Listen request timed out."}):"success"===s.status?e({status:"success"}):e({status:"failed",reason:s.reason}))})}setupEventListener(){return this._socket.on("update",this.emit.bind(this,"update")),this}async subscribe(e){if(!Array.isArray(e)||0===e.length)throw new Error("fields must be a non-empty array");if(this._fields)for(const t of e)this._fields.includes(t)||this._fields.push(t);else this._fields=[...e];return this._updateSubscription()}async unsubscribe(e){if(!Array.isArray(e)||0===e.length)throw new Error("fields must be a non-empty array");if(!this._fields)throw new Error("Cannot unsubscribe when receiving all fields. Use subscribe() first to set explicit field list.");return this._fields=this._fields.filter(t=>!e.includes(t)),this._updateSubscription()}getFields(){return this._fields?[...this._fields]:null}async sendCommand(e,t){if(!e||"string"!=typeof e)throw new Error("field must be a non-empty string");return new Promise(s=>{const i={userId:this._config.userId,gameId:this._config.gameId,data:{fieldName:e,value:t}};this._socket.timeout(5e3).emit("set",i,(e,t)=>s(e?{status:"failed",reason:"Command request timed out."}:t))})}async _updateSubscription(){return new Promise(e=>{const t={userId:this._config.userId,gameId:this._config.gameId,fields:this._fields};this._socket.timeout(5e3).emit("listen-update",t,(t,s)=>e(t?{status:"failed",reason:"Update request timed out."}:s))})}}h(u.prototype);const d={msfs:!0};class l extends a{constructor(e){super(e),this._socket=!1}async auth(){return await this.authenticate(),await this.isAuthenticated()&&await this.initialize(),this.getUserId()}async initialize(){return new Promise(e=>{const t=this.getAccessToken();this._socket=s.io("https://socks.gameglue.gg",{transports:["websocket"],auth:{token:t}}),this._socket.on("connect",()=>{e()}),this.onTokenRefreshed(this.updateSocketAuth)})}updateSocketAuth(e){this._socket.auth.token=e}async createListener(e){if(!e)throw new Error("Not a valid listener config");if(!e.gameId||!d[e.gameId])throw new Error("Not a valid Game ID");if(!e.userId)throw new Error("User ID not supplied");if(e.fields&&!Array.isArray(e.fields))throw new Error("fields must be an array");this._socket||await this.initialize();const t=new u(this._socket,e),s=await t.establishConnection();if(this._socket.io.on("reconnect_attempt",e=>{console.log("Refresh Attempt"),this.updateSocketAuth(this.getAccessToken())}),this._socket.io.on("reconnect",()=>{t.establishConnection()}),"success"!==s.status)throw new Error(`There was a problem setting up the listener. Reason: ${s.reason}`);return t.setupEventListener()}}"undefined"!=typeof window&&(window.GameGlue=l),exports.GameGlue=l,exports.default=l;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("oidc-client-ts"),t=require("jwt-decode"),s=require("socket.io-client"),i=require("event-emitter");const o={},r=(e,t)=>a()?localStorage.setItem(e,t):o[e]=t,n=e=>a()?localStorage.getItem(e):o[e],c=e=>a()?localStorage.removeItem(e):delete o[e],a=()=>!("object"==typeof process&&"[object process]"===String(process));let l=null;class h{constructor(t){const s=t.authUrl||"https://auth.gameglue.gg/realms/GameGlue";this._oidcSettings={authority:s,client_id:t.clientId,redirect_uri:u(t.redirect_uri||window.location.href),post_logout_redirect_uri:u(window.location.href),response_type:"code",scope:`openid ${(t.scopes||[]).join(" ")}`,response_mode:"fragment",filterProtocolClaims:!0},this._oidcClient=new e.OidcClient(this._oidcSettings),this._refreshCallback=()=>{},this._refreshTimeout=null}async isAuthenticated(){return this._hasCallbackParams()&&await this._processCallback(),this._hasValidTokens()}login(){this._oidcClient.createSigninRequest({state:{bar:15}}).then(e=>{window.location=e.url}).catch(e=>{console.error("Failed to create signin request:",e)})}logout(e={}){if(c("gg-auth-token"),c("gg-refresh-token"),clearTimeout(this._refreshTimeout),!1!==e.redirect){const e=`${this._oidcSettings.authority}/protocol/openid-connect/logout?post_logout_redirect_uri=${encodeURIComponent(this._oidcSettings.post_logout_redirect_uri)}`;window.location.href=e}}getUser(){const e=this._getAccessToken();if(!e)throw new Error("Not authenticated");return t(e).sub}getAccessToken(){return this._getAccessToken()}onTokenRefreshed(e){this._refreshCallback=e}_hasCallbackParams(){return location.hash.includes("state=")&&(location.hash.includes("code=")||location.hash.includes("error="))}_clearCallbackUrl(){window.history.replaceState("",document.title,window.location.pathname+window.location.search)}async _processCallback(){if(l)await l;else{l=this._doProcessCallback();try{await l}finally{l=null}}}async _doProcessCallback(){try{const e=await this._oidcClient.processSigninResponse(window.location.href);if(e.error)throw this._clearCallbackUrl(),new Error(e.error);if(!e.access_token)throw this._clearCallbackUrl(),new Error("No access token received");this._setAccessToken(e.access_token),this._setRefreshToken(e.refresh_token),this._clearCallbackUrl()}catch(e){if(this._hasValidTokens())return void this._clearCallbackUrl();throw this._clearCallbackUrl(),e}}_hasValidTokens(){const e=this._getAccessToken();if(!e)return!1;try{const s=t(e);return new Date(1e3*s.exp)>new Date}catch{return!1}}_getAccessToken(){const e=n("gg-auth-token");return e&&this._setTokenRefreshTimeout(e),e}_setAccessToken(e){return this._setTokenRefreshTimeout(e),r("gg-auth-token",e)}_setRefreshToken(e){return r("gg-refresh-token",e)}_getRefreshToken(){return n("gg-refresh-token")}_setTokenRefreshTimeout(e){if(e){clearTimeout(this._refreshTimeout);try{const s=1e3*t(e).exp-Date.now()-5e3;s>0&&(this._refreshTimeout=setTimeout(()=>{this._attemptRefresh()},s))}catch{}}}async _attemptRefresh(){const e=`${this._oidcSettings.authority}/protocol/openid-connect/token`,t=this._oidcSettings.client_id,s=this._getRefreshToken();try{const i=await fetch(e,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:t,grant_type:"refresh_token",refresh_token:s})});if(200===i.status){const e=await i.json();this._setAccessToken(e.access_token),this._setRefreshToken(e.refresh_token),this._refreshCallback(e.access_token)}}catch(e){console.error("Token refresh failed:",e)}}}function u(e){return e.endsWith("/")?e.replace(/\/+$/,""):e}class d{constructor(e,t){this._config=t,this._socket=e,this._callbacks=[],this._fields=t.fields?[...t.fields]:null}async establishConnection(){if(!this._socket||!this._config.userId||!this._config.gameId)throw new Error("Missing arguments in establishConnection");return new Promise(e=>{let t;t=this._fields?{userId:this._config.userId,gameId:this._config.gameId,fields:this._fields}:`${this._config.userId}:${this._config.gameId}`,this._socket.timeout(5e3).emit("listen",t,(t,s)=>t?e({status:"failed",reason:"Listen request timed out."}):"success"===s.status?e({status:"success"}):e({status:"failed",reason:s.reason}))})}setupEventListener(){return this._socket.on("update",e=>{if(this._fields&&this._fields.length>0&&e?.data){const t={};for(const s of this._fields)s in e.data&&(t[s]=e.data[s]);this.emit("update",{...e,data:t})}else this.emit("update",e)}),this}async subscribe(e){if(!Array.isArray(e)||0===e.length)throw new Error("fields must be a non-empty array");if(this._fields)for(const t of e)this._fields.includes(t)||this._fields.push(t);else this._fields=[...e];return this._updateSubscription()}async unsubscribe(e){if(!Array.isArray(e)||0===e.length)throw new Error("fields must be a non-empty array");if(!this._fields)throw new Error("Cannot unsubscribe when receiving all fields. Use subscribe() first to set explicit field list.");return this._fields=this._fields.filter(t=>!e.includes(t)),this._updateSubscription()}getFields(){return this._fields?[...this._fields]:null}async sendCommand(e,t){if(!e||"string"!=typeof e)throw new Error("field must be a non-empty string");return new Promise(s=>{const i={userId:this._config.userId,gameId:this._config.gameId,data:{fieldName:e,value:t}};this._socket.timeout(5e3).emit("set",i,(e,t)=>s(e?{status:"failed",reason:"Command request timed out."}:t))})}async _updateSubscription(){return new Promise(e=>{const t={userId:this._config.userId,gameId:this._config.gameId,fields:this._fields};this._socket.timeout(5e3).emit("listen-update",t,(t,s)=>e(t?{status:"failed",reason:"Update request timed out."}:s))})}}i(d.prototype);const _={msfs:!0};class f extends h{constructor(e){super(e),this._socket=null,this._socketUrl=e.socketUrl||"https://socks.gameglue.gg",this._connectPromise=null}async createListener(e){if(!e)throw new Error("Not a valid listener config");if(!e.gameId||!_[e.gameId])throw new Error("Not a valid Game ID");if(!e.userId)throw new Error("User ID not supplied");if(e.fields&&!Array.isArray(e.fields))throw new Error("fields must be an array");await this._ensureConnected();const t=new d(this._socket,e),s=await t.establishConnection();if(this._socket.io.on("reconnect_attempt",()=>{this._updateSocketAuth(this.getAccessToken())}),this._socket.io.on("reconnect",()=>{t.establishConnection()}),"success"!==s.status)throw new Error(`There was a problem setting up the listener. Reason: ${s.reason}`);return t.setupEventListener()}async _ensureConnected(){if(!this._socket?.connected)if(this._connectPromise)await this._connectPromise;else{this._connectPromise=this._connect();try{await this._connectPromise}finally{this._connectPromise=null}}}_connect(){return new Promise((e,t)=>{const i=this.getAccessToken();i?(this._socket=s.io(this._socketUrl,{transports:["websocket"],auth:{token:i}}),this._socket.on("connect",()=>{e()}),this._socket.on("connect_error",e=>{t(new Error(`Socket connection failed: ${e.message}`))}),this.onTokenRefreshed(e=>{this._updateSocketAuth(e)})):t(new Error("Not authenticated - call isAuthenticated() first"))})}_updateSocketAuth(e){this._socket&&(this._socket.auth.token=e)}}"undefined"!=typeof window&&(window.GameGlue=f),exports.GameGlue=f,exports.default=f;
2
2
  //# sourceMappingURL=gg.cjs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"gg.cjs.js","sources":["../src/utils.js","../src/auth.js","../src/listener.js","../src/index.js"],"sourcesContent":["const storageMap = {};\nexport const storage = {\n set: (key, value) => {\n return isBrowser() ? localStorage.setItem(key, value) : (storageMap[key] = value);\n },\n get: (key) => {\n return isBrowser() ? localStorage.getItem(key) : storageMap[key];\n }\n};\nexport const isBrowser = () => {\n return !(typeof process === 'object' && String(process) === '[object process]');\n}","import { OidcClient } from 'oidc-client-ts';\nimport { storage } from './utils';\nimport jwt_decode from 'jwt-decode';\n\n\nexport class GameGlueAuth {\n constructor(cfg) {\n this._oidcSettings = {\n authority: \"https://auth.gameglue.gg/realms/GameGlue\",\n client_id: cfg.clientId,\n redirect_uri: removeTrailingSlashes(cfg.redirect_uri || window.location.href),\n post_logout_redirect_uri: removeTrailingSlashes(window.location.href),\n response_type: \"code\",\n scope: `openid ${(cfg.scopes||[]).join(' ')}`,\n response_mode: \"fragment\",\n filterProtocolClaims: true\n };\n this._oidcClient = new OidcClient(this._oidcSettings);\n this._refreshCallback = () => {}\n this._refreshTimeout = null;\n }\n setTokenRefreshTimeout(token) {\n if (!token) {\n return;\n }\n clearTimeout(this._refreshTimeout);\n const timeUntilExp = (jwt_decode(token).exp * 1000) - Date.now() - 5000;\n if (timeUntilExp > 0) {\n this._refreshTimeout = setTimeout(() => {\n this.attemptRefresh();\n }, timeUntilExp);\n }\n }\n setAccessToken(token) {\n this.setTokenRefreshTimeout(token);\n return storage.set('gg-auth-token', token);\n }\n getAccessToken() {\n let token = storage.get('gg-auth-token');\n this.setTokenRefreshTimeout(token);\n return token;\n }\n getUserId() {\n const decoded = jwt_decode(this.getAccessToken());\n return decoded.sub;\n }\n setRefreshToken(token) {\n return storage.set('gg-refresh-token', token);\n }\n getRefreshToken(token) {\n return storage.get('gg-refresh-token');\n }\n _shouldHandleRedirectResponse() {\n return (location.hash.includes(\"state=\") && (location.hash.includes(\"code=\") || location.hash.includes(\"error=\")));\n }\n async handleRedirectResponse() {\n let response = await this._oidcClient.processSigninResponse(window.location.href);\n if (response.error || !response.access_token) {\n console.error(response.error);\n return;\n }\n window.history.pushState(\"\", document.title, window.location.pathname + window.location.search);\n this.setAccessToken(response.access_token);\n this.setRefreshToken(response.refresh_token);\n }\n onTokenRefreshed(callback) {\n this._refreshCallback = callback;\n }\n async isAuthenticated(refreshAttempted) {\n // 1. Get the access token\n let access_token = this.getAccessToken();\n \n // 2. If we don't have an access token, we're not authenticated\n if (!access_token) {\n return false;\n }\n // 3. Decode the token, then check to see if it has expired\n const decoded = jwt_decode(access_token);\n const expirationDate = new Date(decoded.exp*1000);\n const isExpired = (expirationDate < new Date());\n \n if (isExpired && !refreshAttempted) {\n await this.attemptRefresh();\n return this.isAuthenticated(true);\n }\n \n // This line might be a little confusing. Basically it's just saying if we tried to refresh the token,\n // but it's STILL expired, return false, otherwise return true.\n return !(isExpired && refreshAttempted);\n }\n isTokenExpired(token) {\n const decoded = jwt_decode(token);\n const expirationDate = new Date(decoded.exp*1000);\n return (expirationDate < new Date());\n }\n async attemptRefresh() {\n const url = `${this._oidcSettings.authority}/protocol/openid-connect/token`;\n const client_id = this._oidcSettings.client_id;\n const refresh_token = this.getRefreshToken();\n const grant_type = 'refresh_token';\n \n try {\n const response = await fetch(url, {\n method: 'POST',\n headers:{\n 'Content-Type': 'application/x-www-form-urlencoded'\n },\n body: new URLSearchParams({\n client_id,\n grant_type,\n refresh_token\n })\n });\n if (response.status === 200) {\n const resObj = await response.json();\n this.setAccessToken(resObj.access_token);\n this.setRefreshToken(resObj.refresh_token);\n this._refreshCallback(resObj);\n }\n } catch(e) {\n console.log('Error: ', e);\n }\n }\n _triggerAuthRedirect() {\n this._oidcClient.createSigninRequest({ state: { bar: 15 } }).then(function(req) {\n window.location = req.url;\n }).catch(function(err) {\n console.error(err);\n });\n }\n async authenticate() {\n if (this._shouldHandleRedirectResponse()) {\n await this.handleRedirectResponse();\n }\n \n let isAuthenticated = await this.isAuthenticated();\n if (!isAuthenticated) {\n await this._triggerAuthRedirect();\n }\n }\n}\n\nfunction removeTrailingSlashes(url) {\n if (url.endsWith('/')) {\n return url.replace(/\\/+$/, '');\n }\n return url;\n}","const EventEmitter = require('event-emitter');\n\nexport class Listener {\n constructor(socket, config) {\n this._config = config;\n this._socket = socket;\n this._callbacks = [];\n this._fields = config.fields ? [...config.fields] : null;\n }\n\n async establishConnection() {\n if (!this._socket || !this._config.userId || !this._config.gameId) {\n throw new Error('Missing arguments in establishConnection');\n }\n return new Promise((resolve) => {\n // Use object format if fields are specified, otherwise use legacy string format\n let listenPayload;\n if (this._fields) {\n listenPayload = {\n userId: this._config.userId,\n gameId: this._config.gameId,\n fields: this._fields\n };\n } else {\n listenPayload = `${this._config.userId}:${this._config.gameId}`;\n }\n\n this._socket.timeout(5000).emit('listen', listenPayload, (error, response) => {\n if (error) {\n return resolve({status: 'failed', reason: 'Listen request timed out.'});\n }\n if (response.status === 'success') {\n return resolve({status: 'success'});\n } else {\n return resolve({status: 'failed', reason: response.reason});\n }\n });\n });\n }\n\n setupEventListener() {\n this._socket.on('update', this.emit.bind(this, 'update'));\n return this;\n }\n\n /**\n * Subscribe to additional fields dynamically\n * @param {string[]} fields - Array of field names to add\n * @returns {Promise<{status: string, reason?: string}>}\n */\n async subscribe(fields) {\n if (!Array.isArray(fields) || fields.length === 0) {\n throw new Error('fields must be a non-empty array');\n }\n\n // Add new fields to existing list\n if (!this._fields) {\n this._fields = [...fields];\n } else {\n for (const field of fields) {\n if (!this._fields.includes(field)) {\n this._fields.push(field);\n }\n }\n }\n\n return this._updateSubscription();\n }\n\n /**\n * Unsubscribe from specific fields\n * @param {string[]} fields - Array of field names to remove\n * @returns {Promise<{status: string, reason?: string}>}\n */\n async unsubscribe(fields) {\n if (!Array.isArray(fields) || fields.length === 0) {\n throw new Error('fields must be a non-empty array');\n }\n\n if (!this._fields) {\n // Currently receiving all fields, create explicit list without these fields\n throw new Error('Cannot unsubscribe when receiving all fields. Use subscribe() first to set explicit field list.');\n }\n\n this._fields = this._fields.filter(f => !fields.includes(f));\n\n return this._updateSubscription();\n }\n\n /**\n * Get the current list of subscribed fields\n * @returns {string[]|null} - Array of field names, or null if receiving all fields\n */\n getFields() {\n return this._fields ? [...this._fields] : null;\n }\n\n /**\n * Send a command to the broadcaster (game client)\n * @param {string} field - The field/action name to set\n * @param {any} value - The value to set\n * @returns {Promise<{status: string, reason?: string}>}\n */\n async sendCommand(field, value) {\n if (!field || typeof field !== 'string') {\n throw new Error('field must be a non-empty string');\n }\n\n return new Promise((resolve) => {\n const payload = {\n userId: this._config.userId,\n gameId: this._config.gameId,\n data: {\n fieldName: field,\n value\n }\n };\n\n this._socket.timeout(5000).emit('set', payload, (error, response) => {\n if (error) {\n return resolve({ status: 'failed', reason: 'Command request timed out.' });\n }\n return resolve(response);\n });\n });\n }\n\n /**\n * Internal method to send subscription update to server\n */\n async _updateSubscription() {\n return new Promise((resolve) => {\n const payload = {\n userId: this._config.userId,\n gameId: this._config.gameId,\n fields: this._fields\n };\n\n this._socket.timeout(5000).emit('listen-update', payload, (error, response) => {\n if (error) {\n return resolve({status: 'failed', reason: 'Update request timed out.'});\n }\n return resolve(response);\n });\n });\n }\n}\n\nEventEmitter(Listener.prototype);","import { GameGlueAuth } from './auth';\nimport { io } from \"socket.io-client\";\nimport { Listener} from \"./listener\";\n\nconst GAME_IDS = {\n 'msfs': true,\n};\n\nclass GameGlue extends GameGlueAuth {\n constructor(cfg) {\n super(cfg);\n this._socket = false;\n }\n \n async auth() {\n await this.authenticate();\n if (await this.isAuthenticated()) {\n await this.initialize();\n }\n return this.getUserId();\n }\n \n async initialize() {\n return new Promise((resolve) => {\n const token = this.getAccessToken();\n // For local development, use 'http://localhost:3031'\n this._socket = io('https://socks.gameglue.gg', {\n transports: ['websocket'],\n auth: {\n token\n }\n });\n // TODO: Update this code to use the new refresh logic. Example in gg-client repo\n this._socket.on('connect', () => {\n resolve();\n });\n this.onTokenRefreshed(this.updateSocketAuth);\n });\n }\n \n updateSocketAuth(authToken) {\n this._socket.auth.token = authToken;\n }\n \n async createListener(config) {\n if (!config) throw new Error('Not a valid listener config');\n if (!config.gameId || !GAME_IDS[config.gameId]) throw new Error('Not a valid Game ID');\n if (!config.userId) throw new Error('User ID not supplied');\n if (config.fields && !Array.isArray(config.fields)) throw new Error('fields must be an array');\n\n // Ensure socket is initialized (handles page reload case)\n if (!this._socket) {\n await this.initialize();\n }\n\n const listener = new Listener(this._socket, config);\n const establishConnectionResponse = await listener.establishConnection();\n this._socket.io.on('reconnect_attempt', (d) => {\n console.log('Refresh Attempt');\n this.updateSocketAuth(this.getAccessToken());\n });\n this._socket.io.on('reconnect', () => {\n listener.establishConnection();\n });\n \n if (establishConnectionResponse.status !== 'success') {\n throw new Error(`There was a problem setting up the listener. Reason: ${establishConnectionResponse.reason}`);\n }\n \n return listener.setupEventListener();\n }\n}\n\nif (typeof window !== 'undefined') {\n window.GameGlue = GameGlue;\n}\n\nexport default GameGlue;\nexport { GameGlue };"],"names":["storageMap","storage","key","value","isBrowser","localStorage","setItem","getItem","process","String","GameGlueAuth","constructor","cfg","this","_oidcSettings","authority","client_id","clientId","redirect_uri","removeTrailingSlashes","window","location","href","post_logout_redirect_uri","response_type","scope","scopes","join","response_mode","filterProtocolClaims","_oidcClient","OidcClient","_refreshCallback","_refreshTimeout","setTokenRefreshTimeout","token","clearTimeout","timeUntilExp","jwt_decode","exp","Date","now","setTimeout","attemptRefresh","setAccessToken","getAccessToken","getUserId","sub","setRefreshToken","getRefreshToken","_shouldHandleRedirectResponse","hash","includes","handleRedirectResponse","response","processSigninResponse","error","access_token","history","pushState","document","title","pathname","search","refresh_token","console","onTokenRefreshed","callback","isAuthenticated","refreshAttempted","decoded","isExpired","isTokenExpired","url","fetch","method","headers","body","URLSearchParams","grant_type","status","resObj","json","e","log","_triggerAuthRedirect","createSigninRequest","state","bar","then","req","catch","err","authenticate","endsWith","replace","EventEmitter","require","Listener","socket","config","_config","_socket","_callbacks","_fields","fields","establishConnection","userId","gameId","Error","Promise","resolve","listenPayload","timeout","emit","reason","setupEventListener","on","bind","subscribe","Array","isArray","length","field","push","_updateSubscription","unsubscribe","filter","f","getFields","sendCommand","payload","data","fieldName","prototype","GAME_IDS","msfs","GameGlue","super","auth","initialize","io","transports","updateSocketAuth","authToken","createListener","listener","establishConnectionResponse","d"],"mappings":"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"}
1
+ {"version":3,"file":"gg.cjs.js","sources":["../src/utils.js","../src/auth.js","../src/listener.js","../src/index.js"],"sourcesContent":["const storageMap = {};\nexport const storage = {\n set: (key, value) => {\n return isBrowser() ? localStorage.setItem(key, value) : (storageMap[key] = value);\n },\n get: (key) => {\n return isBrowser() ? localStorage.getItem(key) : storageMap[key];\n },\n remove: (key) => {\n return isBrowser() ? localStorage.removeItem(key) : delete storageMap[key];\n }\n};\nexport const isBrowser = () => {\n return !(typeof process === 'object' && String(process) === '[object process]');\n}","import { OidcClient } from 'oidc-client-ts';\nimport { storage } from './utils';\nimport jwt_decode from 'jwt-decode';\n\nconst DEFAULT_AUTH_URL = 'https://auth.gameglue.gg/realms/GameGlue';\n\n// Track if callback is being processed (prevents double-processing)\nlet _callbackPromise = null;\n\nexport class GameGlueAuth {\n constructor(cfg) {\n const authority = cfg.authUrl || DEFAULT_AUTH_URL;\n this._oidcSettings = {\n authority,\n client_id: cfg.clientId,\n redirect_uri: removeTrailingSlashes(cfg.redirect_uri || window.location.href),\n post_logout_redirect_uri: removeTrailingSlashes(window.location.href),\n response_type: \"code\",\n scope: `openid ${(cfg.scopes || []).join(' ')}`,\n response_mode: \"fragment\",\n filterProtocolClaims: true\n };\n this._oidcClient = new OidcClient(this._oidcSettings);\n this._refreshCallback = () => {};\n this._refreshTimeout = null;\n }\n\n /**\n * Check if user is authenticated.\n * If OAuth callback params are in URL, processes them first.\n * Safe to call multiple times - idempotent.\n * @returns {Promise<boolean>}\n */\n async isAuthenticated() {\n // If callback params present, process them first\n if (this._hasCallbackParams()) {\n await this._processCallback();\n }\n\n // Check for valid tokens\n return this._hasValidTokens();\n }\n\n /**\n * Redirect to OAuth login page.\n * Does not return - navigates away.\n */\n login() {\n this._oidcClient.createSigninRequest({ state: { bar: 15 } }).then((req) => {\n window.location = req.url;\n }).catch((err) => {\n console.error('Failed to create signin request:', err);\n });\n }\n\n /**\n * Log out the user.\n * Clears local tokens and optionally redirects to Keycloak logout.\n * @param {Object} options - { redirect?: boolean }\n */\n logout(options = {}) {\n // Clear local tokens\n storage.remove('gg-auth-token');\n storage.remove('gg-refresh-token');\n clearTimeout(this._refreshTimeout);\n\n // Optionally redirect to Keycloak logout\n if (options.redirect !== false) {\n const logoutUrl = `${this._oidcSettings.authority}/protocol/openid-connect/logout?post_logout_redirect_uri=${encodeURIComponent(this._oidcSettings.post_logout_redirect_uri)}`;\n window.location.href = logoutUrl;\n }\n }\n\n /**\n * Get the current user's ID.\n * @throws {Error} if not authenticated\n * @returns {string}\n */\n getUser() {\n const token = this._getAccessToken();\n if (!token) {\n throw new Error('Not authenticated');\n }\n const decoded = jwt_decode(token);\n return decoded.sub;\n }\n\n /**\n * Get the access token for API calls.\n * @returns {string|null}\n */\n getAccessToken() {\n return this._getAccessToken();\n }\n\n /**\n * Register callback for token refresh events.\n * @param {Function} callback\n */\n onTokenRefreshed(callback) {\n this._refreshCallback = callback;\n }\n\n // ============ Internal Methods ============\n\n _hasCallbackParams() {\n return location.hash.includes(\"state=\") &&\n (location.hash.includes(\"code=\") || location.hash.includes(\"error=\"));\n }\n\n _clearCallbackUrl() {\n window.history.replaceState(\"\", document.title, window.location.pathname + window.location.search);\n }\n\n async _processCallback() {\n // If already processing, wait for that to complete\n if (_callbackPromise) {\n await _callbackPromise;\n return;\n }\n\n // Start processing\n _callbackPromise = this._doProcessCallback();\n\n try {\n await _callbackPromise;\n } finally {\n _callbackPromise = null;\n }\n }\n\n async _doProcessCallback() {\n try {\n const response = await this._oidcClient.processSigninResponse(window.location.href);\n\n if (response.error) {\n this._clearCallbackUrl();\n throw new Error(response.error);\n }\n\n if (!response.access_token) {\n this._clearCallbackUrl();\n throw new Error('No access token received');\n }\n\n this._setAccessToken(response.access_token);\n this._setRefreshToken(response.refresh_token);\n this._clearCallbackUrl();\n } catch (err) {\n // If we failed but tokens exist (another call succeeded), that's fine\n if (this._hasValidTokens()) {\n this._clearCallbackUrl();\n return;\n }\n this._clearCallbackUrl();\n throw err;\n }\n }\n\n _hasValidTokens() {\n const token = this._getAccessToken();\n if (!token) {\n return false;\n }\n\n try {\n const decoded = jwt_decode(token);\n const expirationDate = new Date(decoded.exp * 1000);\n return expirationDate > new Date();\n } catch {\n return false;\n }\n }\n\n _getAccessToken() {\n const token = storage.get('gg-auth-token');\n if (token) {\n this._setTokenRefreshTimeout(token);\n }\n return token;\n }\n\n _setAccessToken(token) {\n this._setTokenRefreshTimeout(token);\n return storage.set('gg-auth-token', token);\n }\n\n _setRefreshToken(token) {\n return storage.set('gg-refresh-token', token);\n }\n\n _getRefreshToken() {\n return storage.get('gg-refresh-token');\n }\n\n _setTokenRefreshTimeout(token) {\n if (!token) return;\n\n clearTimeout(this._refreshTimeout);\n\n try {\n const timeUntilExp = (jwt_decode(token).exp * 1000) - Date.now() - 5000;\n if (timeUntilExp > 0) {\n this._refreshTimeout = setTimeout(() => {\n this._attemptRefresh();\n }, timeUntilExp);\n }\n } catch {\n // Invalid token, ignore\n }\n }\n\n async _attemptRefresh() {\n const url = `${this._oidcSettings.authority}/protocol/openid-connect/token`;\n const client_id = this._oidcSettings.client_id;\n const refresh_token = this._getRefreshToken();\n const grant_type = 'refresh_token';\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded'\n },\n body: new URLSearchParams({\n client_id,\n grant_type,\n refresh_token\n })\n });\n\n if (response.status === 200) {\n const resObj = await response.json();\n this._setAccessToken(resObj.access_token);\n this._setRefreshToken(resObj.refresh_token);\n this._refreshCallback(resObj.access_token);\n }\n } catch (e) {\n console.error('Token refresh failed:', e);\n }\n }\n}\n\nfunction removeTrailingSlashes(url) {\n if (url.endsWith('/')) {\n return url.replace(/\\/+$/, '');\n }\n return url;\n}\n","import EventEmitter from 'event-emitter';\n\nexport class Listener {\n constructor(socket, config) {\n this._config = config;\n this._socket = socket;\n this._callbacks = [];\n this._fields = config.fields ? [...config.fields] : null;\n }\n\n async establishConnection() {\n if (!this._socket || !this._config.userId || !this._config.gameId) {\n throw new Error('Missing arguments in establishConnection');\n }\n return new Promise((resolve) => {\n // Use object format if fields are specified, otherwise use legacy string format\n let listenPayload;\n if (this._fields) {\n listenPayload = {\n userId: this._config.userId,\n gameId: this._config.gameId,\n fields: this._fields\n };\n } else {\n listenPayload = `${this._config.userId}:${this._config.gameId}`;\n }\n\n this._socket.timeout(5000).emit('listen', listenPayload, (error, response) => {\n if (error) {\n return resolve({status: 'failed', reason: 'Listen request timed out.'});\n }\n if (response.status === 'success') {\n return resolve({status: 'success'});\n } else {\n return resolve({status: 'failed', reason: response.reason});\n }\n });\n });\n }\n\n setupEventListener() {\n this._socket.on('update', (payload) => {\n // Apply client-side field filtering if fields are specified\n if (this._fields && this._fields.length > 0 && payload?.data) {\n const filteredData = {};\n for (const field of this._fields) {\n if (field in payload.data) {\n filteredData[field] = payload.data[field];\n }\n }\n this.emit('update', { ...payload, data: filteredData });\n } else {\n // No filtering - pass through full payload\n this.emit('update', payload);\n }\n });\n return this;\n }\n\n /**\n * Subscribe to additional fields dynamically\n * @param {string[]} fields - Array of field names to add\n * @returns {Promise<{status: string, reason?: string}>}\n */\n async subscribe(fields) {\n if (!Array.isArray(fields) || fields.length === 0) {\n throw new Error('fields must be a non-empty array');\n }\n\n // Add new fields to existing list\n if (!this._fields) {\n this._fields = [...fields];\n } else {\n for (const field of fields) {\n if (!this._fields.includes(field)) {\n this._fields.push(field);\n }\n }\n }\n\n return this._updateSubscription();\n }\n\n /**\n * Unsubscribe from specific fields\n * @param {string[]} fields - Array of field names to remove\n * @returns {Promise<{status: string, reason?: string}>}\n */\n async unsubscribe(fields) {\n if (!Array.isArray(fields) || fields.length === 0) {\n throw new Error('fields must be a non-empty array');\n }\n\n if (!this._fields) {\n // Currently receiving all fields, create explicit list without these fields\n throw new Error('Cannot unsubscribe when receiving all fields. Use subscribe() first to set explicit field list.');\n }\n\n this._fields = this._fields.filter(f => !fields.includes(f));\n\n return this._updateSubscription();\n }\n\n /**\n * Get the current list of subscribed fields\n * @returns {string[]|null} - Array of field names, or null if receiving all fields\n */\n getFields() {\n return this._fields ? [...this._fields] : null;\n }\n\n /**\n * Send a command to the broadcaster (game client)\n * @param {string} field - The field/action name to set\n * @param {any} value - The value to set\n * @returns {Promise<{status: string, reason?: string}>}\n */\n async sendCommand(field, value) {\n if (!field || typeof field !== 'string') {\n throw new Error('field must be a non-empty string');\n }\n\n return new Promise((resolve) => {\n const payload = {\n userId: this._config.userId,\n gameId: this._config.gameId,\n data: {\n fieldName: field,\n value\n }\n };\n\n this._socket.timeout(5000).emit('set', payload, (error, response) => {\n if (error) {\n return resolve({ status: 'failed', reason: 'Command request timed out.' });\n }\n return resolve(response);\n });\n });\n }\n\n /**\n * Internal method to send subscription update to server\n */\n async _updateSubscription() {\n return new Promise((resolve) => {\n const payload = {\n userId: this._config.userId,\n gameId: this._config.gameId,\n fields: this._fields\n };\n\n this._socket.timeout(5000).emit('listen-update', payload, (error, response) => {\n if (error) {\n return resolve({status: 'failed', reason: 'Update request timed out.'});\n }\n return resolve(response);\n });\n });\n }\n}\n\nEventEmitter(Listener.prototype);","import { GameGlueAuth } from './auth';\nimport { io } from \"socket.io-client\";\nimport { Listener } from \"./listener\";\n\nconst GAME_IDS = {\n 'msfs': true,\n};\n\nconst DEFAULT_SOCKET_URL = 'https://socks.gameglue.gg';\n\nclass GameGlue extends GameGlueAuth {\n constructor(cfg) {\n super(cfg);\n this._socket = null;\n this._socketUrl = cfg.socketUrl || DEFAULT_SOCKET_URL;\n this._connectPromise = null;\n }\n\n /**\n * Create a listener for game telemetry.\n * Connects to socket server lazily on first call.\n * @param {Object} config - { userId, gameId, fields? }\n * @returns {Promise<Listener>}\n */\n async createListener(config) {\n if (!config) throw new Error('Not a valid listener config');\n if (!config.gameId || !GAME_IDS[config.gameId]) throw new Error('Not a valid Game ID');\n if (!config.userId) throw new Error('User ID not supplied');\n if (config.fields && !Array.isArray(config.fields)) throw new Error('fields must be an array');\n\n // Ensure socket is connected (lazy initialization)\n await this._ensureConnected();\n\n const listener = new Listener(this._socket, config);\n const establishConnectionResponse = await listener.establishConnection();\n\n // Handle reconnection\n this._socket.io.on('reconnect_attempt', () => {\n this._updateSocketAuth(this.getAccessToken());\n });\n this._socket.io.on('reconnect', () => {\n listener.establishConnection();\n });\n\n if (establishConnectionResponse.status !== 'success') {\n throw new Error(`There was a problem setting up the listener. Reason: ${establishConnectionResponse.reason}`);\n }\n\n return listener.setupEventListener();\n }\n\n // ============ Internal Methods ============\n\n async _ensureConnected() {\n // Already connected\n if (this._socket?.connected) {\n return;\n }\n\n // Connection in progress - wait for it\n if (this._connectPromise) {\n await this._connectPromise;\n return;\n }\n\n // Start new connection\n this._connectPromise = this._connect();\n\n try {\n await this._connectPromise;\n } finally {\n this._connectPromise = null;\n }\n }\n\n _connect() {\n return new Promise((resolve, reject) => {\n const token = this.getAccessToken();\n\n if (!token) {\n reject(new Error('Not authenticated - call isAuthenticated() first'));\n return;\n }\n\n this._socket = io(this._socketUrl, {\n transports: ['websocket'],\n auth: { token }\n });\n\n this._socket.on('connect', () => {\n resolve();\n });\n\n this._socket.on('connect_error', (err) => {\n reject(new Error(`Socket connection failed: ${err.message}`));\n });\n\n // Update socket auth when token refreshes\n this.onTokenRefreshed((newToken) => {\n this._updateSocketAuth(newToken);\n });\n });\n }\n\n _updateSocketAuth(authToken) {\n if (this._socket) {\n this._socket.auth.token = authToken;\n }\n }\n}\n\nif (typeof window !== 'undefined') {\n window.GameGlue = GameGlue;\n}\n\nexport default GameGlue;\nexport { GameGlue };\n"],"names":["storageMap","storage","key","value","isBrowser","localStorage","setItem","getItem","removeItem","process","String","_callbackPromise","GameGlueAuth","constructor","cfg","authority","authUrl","this","_oidcSettings","client_id","clientId","redirect_uri","removeTrailingSlashes","window","location","href","post_logout_redirect_uri","response_type","scope","scopes","join","response_mode","filterProtocolClaims","_oidcClient","OidcClient","_refreshCallback","_refreshTimeout","isAuthenticated","_hasCallbackParams","_processCallback","_hasValidTokens","login","createSigninRequest","state","bar","then","req","url","catch","err","console","error","logout","options","clearTimeout","redirect","logoutUrl","encodeURIComponent","getUser","token","_getAccessToken","Error","jwt_decode","sub","getAccessToken","onTokenRefreshed","callback","hash","includes","_clearCallbackUrl","history","replaceState","document","title","pathname","search","_doProcessCallback","response","processSigninResponse","access_token","_setAccessToken","_setRefreshToken","refresh_token","decoded","Date","exp","_setTokenRefreshTimeout","_getRefreshToken","timeUntilExp","now","setTimeout","_attemptRefresh","fetch","method","headers","body","URLSearchParams","grant_type","status","resObj","json","e","endsWith","replace","Listener","socket","config","_config","_socket","_callbacks","_fields","fields","establishConnection","userId","gameId","Promise","resolve","listenPayload","timeout","emit","reason","setupEventListener","on","payload","length","data","filteredData","field","subscribe","Array","isArray","push","_updateSubscription","unsubscribe","filter","f","getFields","sendCommand","fieldName","EventEmitter","prototype","GAME_IDS","msfs","GameGlue","super","_socketUrl","socketUrl","_connectPromise","createListener","_ensureConnected","listener","establishConnectionResponse","io","_updateSocketAuth","connected","_connect","reject","transports","auth","message","newToken","authToken"],"mappings":"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"}
package/dist/gg.esm.js CHANGED
@@ -1,2 +1,2 @@
1
- import{OidcClient as e}from"oidc-client-ts";import t from"jwt-decode";import{io as s}from"socket.io-client";const i={},o=(e,t)=>r()?localStorage.setItem(e,t):i[e]=t,n=e=>r()?localStorage.getItem(e):i[e],r=()=>!("object"==typeof process&&"[object process]"===String(process));class a{constructor(t){this._oidcSettings={authority:"https://auth.gameglue.gg/realms/GameGlue",client_id:t.clientId,redirect_uri:c(t.redirect_uri||window.location.href),post_logout_redirect_uri:c(window.location.href),response_type:"code",scope:`openid ${(t.scopes||[]).join(" ")}`,response_mode:"fragment",filterProtocolClaims:!0},this._oidcClient=new e(this._oidcSettings),this._refreshCallback=()=>{},this._refreshTimeout=null}setTokenRefreshTimeout(e){if(!e)return;clearTimeout(this._refreshTimeout);const s=1e3*t(e).exp-Date.now()-5e3;s>0&&(this._refreshTimeout=setTimeout(()=>{this.attemptRefresh()},s))}setAccessToken(e){return this.setTokenRefreshTimeout(e),o("gg-auth-token",e)}getAccessToken(){let e=n("gg-auth-token");return this.setTokenRefreshTimeout(e),e}getUserId(){return t(this.getAccessToken()).sub}setRefreshToken(e){return o("gg-refresh-token",e)}getRefreshToken(e){return n("gg-refresh-token")}_shouldHandleRedirectResponse(){return location.hash.includes("state=")&&(location.hash.includes("code=")||location.hash.includes("error="))}async handleRedirectResponse(){let e=await this._oidcClient.processSigninResponse(window.location.href);!e.error&&e.access_token?(window.history.pushState("",document.title,window.location.pathname+window.location.search),this.setAccessToken(e.access_token),this.setRefreshToken(e.refresh_token)):console.error(e.error)}onTokenRefreshed(e){this._refreshCallback=e}async isAuthenticated(e){let s=this.getAccessToken();if(!s)return!1;const i=t(s),o=new Date(1e3*i.exp)<new Date;return o&&!e?(await this.attemptRefresh(),this.isAuthenticated(!0)):!(o&&e)}isTokenExpired(e){const s=t(e);return new Date(1e3*s.exp)<new Date}async attemptRefresh(){const e=`${this._oidcSettings.authority}/protocol/openid-connect/token`,t=this._oidcSettings.client_id,s=this.getRefreshToken();try{const i=await fetch(e,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:t,grant_type:"refresh_token",refresh_token:s})});if(200===i.status){const e=await i.json();this.setAccessToken(e.access_token),this.setRefreshToken(e.refresh_token),this._refreshCallback(e)}}catch(e){console.log("Error: ",e)}}_triggerAuthRedirect(){this._oidcClient.createSigninRequest({state:{bar:15}}).then(function(e){window.location=e.url}).catch(function(e){console.error(e)})}async authenticate(){this._shouldHandleRedirectResponse()&&await this.handleRedirectResponse(),await this.isAuthenticated()||await this._triggerAuthRedirect()}}function c(e){return e.endsWith("/")?e.replace(/\/+$/,""):e}const h=require("event-emitter");class u{constructor(e,t){this._config=t,this._socket=e,this._callbacks=[],this._fields=t.fields?[...t.fields]:null}async establishConnection(){if(!this._socket||!this._config.userId||!this._config.gameId)throw new Error("Missing arguments in establishConnection");return new Promise(e=>{let t;t=this._fields?{userId:this._config.userId,gameId:this._config.gameId,fields:this._fields}:`${this._config.userId}:${this._config.gameId}`,this._socket.timeout(5e3).emit("listen",t,(t,s)=>t?e({status:"failed",reason:"Listen request timed out."}):"success"===s.status?e({status:"success"}):e({status:"failed",reason:s.reason}))})}setupEventListener(){return this._socket.on("update",this.emit.bind(this,"update")),this}async subscribe(e){if(!Array.isArray(e)||0===e.length)throw new Error("fields must be a non-empty array");if(this._fields)for(const t of e)this._fields.includes(t)||this._fields.push(t);else this._fields=[...e];return this._updateSubscription()}async unsubscribe(e){if(!Array.isArray(e)||0===e.length)throw new Error("fields must be a non-empty array");if(!this._fields)throw new Error("Cannot unsubscribe when receiving all fields. Use subscribe() first to set explicit field list.");return this._fields=this._fields.filter(t=>!e.includes(t)),this._updateSubscription()}getFields(){return this._fields?[...this._fields]:null}async sendCommand(e,t){if(!e||"string"!=typeof e)throw new Error("field must be a non-empty string");return new Promise(s=>{const i={userId:this._config.userId,gameId:this._config.gameId,data:{fieldName:e,value:t}};this._socket.timeout(5e3).emit("set",i,(e,t)=>s(e?{status:"failed",reason:"Command request timed out."}:t))})}async _updateSubscription(){return new Promise(e=>{const t={userId:this._config.userId,gameId:this._config.gameId,fields:this._fields};this._socket.timeout(5e3).emit("listen-update",t,(t,s)=>e(t?{status:"failed",reason:"Update request timed out."}:s))})}}h(u.prototype);const d={msfs:!0};class l extends a{constructor(e){super(e),this._socket=!1}async auth(){return await this.authenticate(),await this.isAuthenticated()&&await this.initialize(),this.getUserId()}async initialize(){return new Promise(e=>{const t=this.getAccessToken();this._socket=s("https://socks.gameglue.gg",{transports:["websocket"],auth:{token:t}}),this._socket.on("connect",()=>{e()}),this.onTokenRefreshed(this.updateSocketAuth)})}updateSocketAuth(e){this._socket.auth.token=e}async createListener(e){if(!e)throw new Error("Not a valid listener config");if(!e.gameId||!d[e.gameId])throw new Error("Not a valid Game ID");if(!e.userId)throw new Error("User ID not supplied");if(e.fields&&!Array.isArray(e.fields))throw new Error("fields must be an array");this._socket||await this.initialize();const t=new u(this._socket,e),s=await t.establishConnection();if(this._socket.io.on("reconnect_attempt",e=>{console.log("Refresh Attempt"),this.updateSocketAuth(this.getAccessToken())}),this._socket.io.on("reconnect",()=>{t.establishConnection()}),"success"!==s.status)throw new Error(`There was a problem setting up the listener. Reason: ${s.reason}`);return t.setupEventListener()}}"undefined"!=typeof window&&(window.GameGlue=l);export{l as GameGlue,l as default};
1
+ import{OidcClient as e}from"oidc-client-ts";import t from"jwt-decode";import{io as s}from"socket.io-client";import i from"event-emitter";const o={},r=(e,t)=>a()?localStorage.setItem(e,t):o[e]=t,n=e=>a()?localStorage.getItem(e):o[e],c=e=>a()?localStorage.removeItem(e):delete o[e],a=()=>!("object"==typeof process&&"[object process]"===String(process));let h=null;class l{constructor(t){const s=t.authUrl||"https://auth.gameglue.gg/realms/GameGlue";this._oidcSettings={authority:s,client_id:t.clientId,redirect_uri:d(t.redirect_uri||window.location.href),post_logout_redirect_uri:d(window.location.href),response_type:"code",scope:`openid ${(t.scopes||[]).join(" ")}`,response_mode:"fragment",filterProtocolClaims:!0},this._oidcClient=new e(this._oidcSettings),this._refreshCallback=()=>{},this._refreshTimeout=null}async isAuthenticated(){return this._hasCallbackParams()&&await this._processCallback(),this._hasValidTokens()}login(){this._oidcClient.createSigninRequest({state:{bar:15}}).then(e=>{window.location=e.url}).catch(e=>{console.error("Failed to create signin request:",e)})}logout(e={}){if(c("gg-auth-token"),c("gg-refresh-token"),clearTimeout(this._refreshTimeout),!1!==e.redirect){const e=`${this._oidcSettings.authority}/protocol/openid-connect/logout?post_logout_redirect_uri=${encodeURIComponent(this._oidcSettings.post_logout_redirect_uri)}`;window.location.href=e}}getUser(){const e=this._getAccessToken();if(!e)throw new Error("Not authenticated");return t(e).sub}getAccessToken(){return this._getAccessToken()}onTokenRefreshed(e){this._refreshCallback=e}_hasCallbackParams(){return location.hash.includes("state=")&&(location.hash.includes("code=")||location.hash.includes("error="))}_clearCallbackUrl(){window.history.replaceState("",document.title,window.location.pathname+window.location.search)}async _processCallback(){if(h)await h;else{h=this._doProcessCallback();try{await h}finally{h=null}}}async _doProcessCallback(){try{const e=await this._oidcClient.processSigninResponse(window.location.href);if(e.error)throw this._clearCallbackUrl(),new Error(e.error);if(!e.access_token)throw this._clearCallbackUrl(),new Error("No access token received");this._setAccessToken(e.access_token),this._setRefreshToken(e.refresh_token),this._clearCallbackUrl()}catch(e){if(this._hasValidTokens())return void this._clearCallbackUrl();throw this._clearCallbackUrl(),e}}_hasValidTokens(){const e=this._getAccessToken();if(!e)return!1;try{const s=t(e);return new Date(1e3*s.exp)>new Date}catch{return!1}}_getAccessToken(){const e=n("gg-auth-token");return e&&this._setTokenRefreshTimeout(e),e}_setAccessToken(e){return this._setTokenRefreshTimeout(e),r("gg-auth-token",e)}_setRefreshToken(e){return r("gg-refresh-token",e)}_getRefreshToken(){return n("gg-refresh-token")}_setTokenRefreshTimeout(e){if(e){clearTimeout(this._refreshTimeout);try{const s=1e3*t(e).exp-Date.now()-5e3;s>0&&(this._refreshTimeout=setTimeout(()=>{this._attemptRefresh()},s))}catch{}}}async _attemptRefresh(){const e=`${this._oidcSettings.authority}/protocol/openid-connect/token`,t=this._oidcSettings.client_id,s=this._getRefreshToken();try{const i=await fetch(e,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:t,grant_type:"refresh_token",refresh_token:s})});if(200===i.status){const e=await i.json();this._setAccessToken(e.access_token),this._setRefreshToken(e.refresh_token),this._refreshCallback(e.access_token)}}catch(e){console.error("Token refresh failed:",e)}}}function d(e){return e.endsWith("/")?e.replace(/\/+$/,""):e}class u{constructor(e,t){this._config=t,this._socket=e,this._callbacks=[],this._fields=t.fields?[...t.fields]:null}async establishConnection(){if(!this._socket||!this._config.userId||!this._config.gameId)throw new Error("Missing arguments in establishConnection");return new Promise(e=>{let t;t=this._fields?{userId:this._config.userId,gameId:this._config.gameId,fields:this._fields}:`${this._config.userId}:${this._config.gameId}`,this._socket.timeout(5e3).emit("listen",t,(t,s)=>t?e({status:"failed",reason:"Listen request timed out."}):"success"===s.status?e({status:"success"}):e({status:"failed",reason:s.reason}))})}setupEventListener(){return this._socket.on("update",e=>{if(this._fields&&this._fields.length>0&&e?.data){const t={};for(const s of this._fields)s in e.data&&(t[s]=e.data[s]);this.emit("update",{...e,data:t})}else this.emit("update",e)}),this}async subscribe(e){if(!Array.isArray(e)||0===e.length)throw new Error("fields must be a non-empty array");if(this._fields)for(const t of e)this._fields.includes(t)||this._fields.push(t);else this._fields=[...e];return this._updateSubscription()}async unsubscribe(e){if(!Array.isArray(e)||0===e.length)throw new Error("fields must be a non-empty array");if(!this._fields)throw new Error("Cannot unsubscribe when receiving all fields. Use subscribe() first to set explicit field list.");return this._fields=this._fields.filter(t=>!e.includes(t)),this._updateSubscription()}getFields(){return this._fields?[...this._fields]:null}async sendCommand(e,t){if(!e||"string"!=typeof e)throw new Error("field must be a non-empty string");return new Promise(s=>{const i={userId:this._config.userId,gameId:this._config.gameId,data:{fieldName:e,value:t}};this._socket.timeout(5e3).emit("set",i,(e,t)=>s(e?{status:"failed",reason:"Command request timed out."}:t))})}async _updateSubscription(){return new Promise(e=>{const t={userId:this._config.userId,gameId:this._config.gameId,fields:this._fields};this._socket.timeout(5e3).emit("listen-update",t,(t,s)=>e(t?{status:"failed",reason:"Update request timed out."}:s))})}}i(u.prototype);const _={msfs:!0};class f extends l{constructor(e){super(e),this._socket=null,this._socketUrl=e.socketUrl||"https://socks.gameglue.gg",this._connectPromise=null}async createListener(e){if(!e)throw new Error("Not a valid listener config");if(!e.gameId||!_[e.gameId])throw new Error("Not a valid Game ID");if(!e.userId)throw new Error("User ID not supplied");if(e.fields&&!Array.isArray(e.fields))throw new Error("fields must be an array");await this._ensureConnected();const t=new u(this._socket,e),s=await t.establishConnection();if(this._socket.io.on("reconnect_attempt",()=>{this._updateSocketAuth(this.getAccessToken())}),this._socket.io.on("reconnect",()=>{t.establishConnection()}),"success"!==s.status)throw new Error(`There was a problem setting up the listener. Reason: ${s.reason}`);return t.setupEventListener()}async _ensureConnected(){if(!this._socket?.connected)if(this._connectPromise)await this._connectPromise;else{this._connectPromise=this._connect();try{await this._connectPromise}finally{this._connectPromise=null}}}_connect(){return new Promise((e,t)=>{const i=this.getAccessToken();i?(this._socket=s(this._socketUrl,{transports:["websocket"],auth:{token:i}}),this._socket.on("connect",()=>{e()}),this._socket.on("connect_error",e=>{t(new Error(`Socket connection failed: ${e.message}`))}),this.onTokenRefreshed(e=>{this._updateSocketAuth(e)})):t(new Error("Not authenticated - call isAuthenticated() first"))})}_updateSocketAuth(e){this._socket&&(this._socket.auth.token=e)}}"undefined"!=typeof window&&(window.GameGlue=f);export{f as GameGlue,f as default};
2
2
  //# sourceMappingURL=gg.esm.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"gg.esm.js","sources":["../src/utils.js","../src/auth.js","../src/listener.js","../src/index.js"],"sourcesContent":["const storageMap = {};\nexport const storage = {\n set: (key, value) => {\n return isBrowser() ? localStorage.setItem(key, value) : (storageMap[key] = value);\n },\n get: (key) => {\n return isBrowser() ? localStorage.getItem(key) : storageMap[key];\n }\n};\nexport const isBrowser = () => {\n return !(typeof process === 'object' && String(process) === '[object process]');\n}","import { OidcClient } from 'oidc-client-ts';\nimport { storage } from './utils';\nimport jwt_decode from 'jwt-decode';\n\n\nexport class GameGlueAuth {\n constructor(cfg) {\n this._oidcSettings = {\n authority: \"https://auth.gameglue.gg/realms/GameGlue\",\n client_id: cfg.clientId,\n redirect_uri: removeTrailingSlashes(cfg.redirect_uri || window.location.href),\n post_logout_redirect_uri: removeTrailingSlashes(window.location.href),\n response_type: \"code\",\n scope: `openid ${(cfg.scopes||[]).join(' ')}`,\n response_mode: \"fragment\",\n filterProtocolClaims: true\n };\n this._oidcClient = new OidcClient(this._oidcSettings);\n this._refreshCallback = () => {}\n this._refreshTimeout = null;\n }\n setTokenRefreshTimeout(token) {\n if (!token) {\n return;\n }\n clearTimeout(this._refreshTimeout);\n const timeUntilExp = (jwt_decode(token).exp * 1000) - Date.now() - 5000;\n if (timeUntilExp > 0) {\n this._refreshTimeout = setTimeout(() => {\n this.attemptRefresh();\n }, timeUntilExp);\n }\n }\n setAccessToken(token) {\n this.setTokenRefreshTimeout(token);\n return storage.set('gg-auth-token', token);\n }\n getAccessToken() {\n let token = storage.get('gg-auth-token');\n this.setTokenRefreshTimeout(token);\n return token;\n }\n getUserId() {\n const decoded = jwt_decode(this.getAccessToken());\n return decoded.sub;\n }\n setRefreshToken(token) {\n return storage.set('gg-refresh-token', token);\n }\n getRefreshToken(token) {\n return storage.get('gg-refresh-token');\n }\n _shouldHandleRedirectResponse() {\n return (location.hash.includes(\"state=\") && (location.hash.includes(\"code=\") || location.hash.includes(\"error=\")));\n }\n async handleRedirectResponse() {\n let response = await this._oidcClient.processSigninResponse(window.location.href);\n if (response.error || !response.access_token) {\n console.error(response.error);\n return;\n }\n window.history.pushState(\"\", document.title, window.location.pathname + window.location.search);\n this.setAccessToken(response.access_token);\n this.setRefreshToken(response.refresh_token);\n }\n onTokenRefreshed(callback) {\n this._refreshCallback = callback;\n }\n async isAuthenticated(refreshAttempted) {\n // 1. Get the access token\n let access_token = this.getAccessToken();\n \n // 2. If we don't have an access token, we're not authenticated\n if (!access_token) {\n return false;\n }\n // 3. Decode the token, then check to see if it has expired\n const decoded = jwt_decode(access_token);\n const expirationDate = new Date(decoded.exp*1000);\n const isExpired = (expirationDate < new Date());\n \n if (isExpired && !refreshAttempted) {\n await this.attemptRefresh();\n return this.isAuthenticated(true);\n }\n \n // This line might be a little confusing. Basically it's just saying if we tried to refresh the token,\n // but it's STILL expired, return false, otherwise return true.\n return !(isExpired && refreshAttempted);\n }\n isTokenExpired(token) {\n const decoded = jwt_decode(token);\n const expirationDate = new Date(decoded.exp*1000);\n return (expirationDate < new Date());\n }\n async attemptRefresh() {\n const url = `${this._oidcSettings.authority}/protocol/openid-connect/token`;\n const client_id = this._oidcSettings.client_id;\n const refresh_token = this.getRefreshToken();\n const grant_type = 'refresh_token';\n \n try {\n const response = await fetch(url, {\n method: 'POST',\n headers:{\n 'Content-Type': 'application/x-www-form-urlencoded'\n },\n body: new URLSearchParams({\n client_id,\n grant_type,\n refresh_token\n })\n });\n if (response.status === 200) {\n const resObj = await response.json();\n this.setAccessToken(resObj.access_token);\n this.setRefreshToken(resObj.refresh_token);\n this._refreshCallback(resObj);\n }\n } catch(e) {\n console.log('Error: ', e);\n }\n }\n _triggerAuthRedirect() {\n this._oidcClient.createSigninRequest({ state: { bar: 15 } }).then(function(req) {\n window.location = req.url;\n }).catch(function(err) {\n console.error(err);\n });\n }\n async authenticate() {\n if (this._shouldHandleRedirectResponse()) {\n await this.handleRedirectResponse();\n }\n \n let isAuthenticated = await this.isAuthenticated();\n if (!isAuthenticated) {\n await this._triggerAuthRedirect();\n }\n }\n}\n\nfunction removeTrailingSlashes(url) {\n if (url.endsWith('/')) {\n return url.replace(/\\/+$/, '');\n }\n return url;\n}","const EventEmitter = require('event-emitter');\n\nexport class Listener {\n constructor(socket, config) {\n this._config = config;\n this._socket = socket;\n this._callbacks = [];\n this._fields = config.fields ? [...config.fields] : null;\n }\n\n async establishConnection() {\n if (!this._socket || !this._config.userId || !this._config.gameId) {\n throw new Error('Missing arguments in establishConnection');\n }\n return new Promise((resolve) => {\n // Use object format if fields are specified, otherwise use legacy string format\n let listenPayload;\n if (this._fields) {\n listenPayload = {\n userId: this._config.userId,\n gameId: this._config.gameId,\n fields: this._fields\n };\n } else {\n listenPayload = `${this._config.userId}:${this._config.gameId}`;\n }\n\n this._socket.timeout(5000).emit('listen', listenPayload, (error, response) => {\n if (error) {\n return resolve({status: 'failed', reason: 'Listen request timed out.'});\n }\n if (response.status === 'success') {\n return resolve({status: 'success'});\n } else {\n return resolve({status: 'failed', reason: response.reason});\n }\n });\n });\n }\n\n setupEventListener() {\n this._socket.on('update', this.emit.bind(this, 'update'));\n return this;\n }\n\n /**\n * Subscribe to additional fields dynamically\n * @param {string[]} fields - Array of field names to add\n * @returns {Promise<{status: string, reason?: string}>}\n */\n async subscribe(fields) {\n if (!Array.isArray(fields) || fields.length === 0) {\n throw new Error('fields must be a non-empty array');\n }\n\n // Add new fields to existing list\n if (!this._fields) {\n this._fields = [...fields];\n } else {\n for (const field of fields) {\n if (!this._fields.includes(field)) {\n this._fields.push(field);\n }\n }\n }\n\n return this._updateSubscription();\n }\n\n /**\n * Unsubscribe from specific fields\n * @param {string[]} fields - Array of field names to remove\n * @returns {Promise<{status: string, reason?: string}>}\n */\n async unsubscribe(fields) {\n if (!Array.isArray(fields) || fields.length === 0) {\n throw new Error('fields must be a non-empty array');\n }\n\n if (!this._fields) {\n // Currently receiving all fields, create explicit list without these fields\n throw new Error('Cannot unsubscribe when receiving all fields. Use subscribe() first to set explicit field list.');\n }\n\n this._fields = this._fields.filter(f => !fields.includes(f));\n\n return this._updateSubscription();\n }\n\n /**\n * Get the current list of subscribed fields\n * @returns {string[]|null} - Array of field names, or null if receiving all fields\n */\n getFields() {\n return this._fields ? [...this._fields] : null;\n }\n\n /**\n * Send a command to the broadcaster (game client)\n * @param {string} field - The field/action name to set\n * @param {any} value - The value to set\n * @returns {Promise<{status: string, reason?: string}>}\n */\n async sendCommand(field, value) {\n if (!field || typeof field !== 'string') {\n throw new Error('field must be a non-empty string');\n }\n\n return new Promise((resolve) => {\n const payload = {\n userId: this._config.userId,\n gameId: this._config.gameId,\n data: {\n fieldName: field,\n value\n }\n };\n\n this._socket.timeout(5000).emit('set', payload, (error, response) => {\n if (error) {\n return resolve({ status: 'failed', reason: 'Command request timed out.' });\n }\n return resolve(response);\n });\n });\n }\n\n /**\n * Internal method to send subscription update to server\n */\n async _updateSubscription() {\n return new Promise((resolve) => {\n const payload = {\n userId: this._config.userId,\n gameId: this._config.gameId,\n fields: this._fields\n };\n\n this._socket.timeout(5000).emit('listen-update', payload, (error, response) => {\n if (error) {\n return resolve({status: 'failed', reason: 'Update request timed out.'});\n }\n return resolve(response);\n });\n });\n }\n}\n\nEventEmitter(Listener.prototype);","import { GameGlueAuth } from './auth';\nimport { io } from \"socket.io-client\";\nimport { Listener} from \"./listener\";\n\nconst GAME_IDS = {\n 'msfs': true,\n};\n\nclass GameGlue extends GameGlueAuth {\n constructor(cfg) {\n super(cfg);\n this._socket = false;\n }\n \n async auth() {\n await this.authenticate();\n if (await this.isAuthenticated()) {\n await this.initialize();\n }\n return this.getUserId();\n }\n \n async initialize() {\n return new Promise((resolve) => {\n const token = this.getAccessToken();\n // For local development, use 'http://localhost:3031'\n this._socket = io('https://socks.gameglue.gg', {\n transports: ['websocket'],\n auth: {\n token\n }\n });\n // TODO: Update this code to use the new refresh logic. Example in gg-client repo\n this._socket.on('connect', () => {\n resolve();\n });\n this.onTokenRefreshed(this.updateSocketAuth);\n });\n }\n \n updateSocketAuth(authToken) {\n this._socket.auth.token = authToken;\n }\n \n async createListener(config) {\n if (!config) throw new Error('Not a valid listener config');\n if (!config.gameId || !GAME_IDS[config.gameId]) throw new Error('Not a valid Game ID');\n if (!config.userId) throw new Error('User ID not supplied');\n if (config.fields && !Array.isArray(config.fields)) throw new Error('fields must be an array');\n\n // Ensure socket is initialized (handles page reload case)\n if (!this._socket) {\n await this.initialize();\n }\n\n const listener = new Listener(this._socket, config);\n const establishConnectionResponse = await listener.establishConnection();\n this._socket.io.on('reconnect_attempt', (d) => {\n console.log('Refresh Attempt');\n this.updateSocketAuth(this.getAccessToken());\n });\n this._socket.io.on('reconnect', () => {\n listener.establishConnection();\n });\n \n if (establishConnectionResponse.status !== 'success') {\n throw new Error(`There was a problem setting up the listener. Reason: ${establishConnectionResponse.reason}`);\n }\n \n return listener.setupEventListener();\n }\n}\n\nif (typeof window !== 'undefined') {\n window.GameGlue = GameGlue;\n}\n\nexport default GameGlue;\nexport { GameGlue };"],"names":["storageMap","storage","key","value","isBrowser","localStorage","setItem","getItem","process","String","GameGlueAuth","constructor","cfg","this","_oidcSettings","authority","client_id","clientId","redirect_uri","removeTrailingSlashes","window","location","href","post_logout_redirect_uri","response_type","scope","scopes","join","response_mode","filterProtocolClaims","_oidcClient","OidcClient","_refreshCallback","_refreshTimeout","setTokenRefreshTimeout","token","clearTimeout","timeUntilExp","jwt_decode","exp","Date","now","setTimeout","attemptRefresh","setAccessToken","getAccessToken","getUserId","sub","setRefreshToken","getRefreshToken","_shouldHandleRedirectResponse","hash","includes","handleRedirectResponse","response","processSigninResponse","error","access_token","history","pushState","document","title","pathname","search","refresh_token","console","onTokenRefreshed","callback","isAuthenticated","refreshAttempted","decoded","isExpired","isTokenExpired","url","fetch","method","headers","body","URLSearchParams","grant_type","status","resObj","json","e","log","_triggerAuthRedirect","createSigninRequest","state","bar","then","req","catch","err","authenticate","endsWith","replace","EventEmitter","require","Listener","socket","config","_config","_socket","_callbacks","_fields","fields","establishConnection","userId","gameId","Error","Promise","resolve","listenPayload","timeout","emit","reason","setupEventListener","on","bind","subscribe","Array","isArray","length","field","push","_updateSubscription","unsubscribe","filter","f","getFields","sendCommand","payload","data","fieldName","prototype","GAME_IDS","msfs","GameGlue","super","auth","initialize","io","transports","updateSocketAuth","authToken","createListener","listener","establishConnectionResponse","d"],"mappings":"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"}
1
+ {"version":3,"file":"gg.esm.js","sources":["../src/utils.js","../src/auth.js","../src/listener.js","../src/index.js"],"sourcesContent":["const storageMap = {};\nexport const storage = {\n set: (key, value) => {\n return isBrowser() ? localStorage.setItem(key, value) : (storageMap[key] = value);\n },\n get: (key) => {\n return isBrowser() ? localStorage.getItem(key) : storageMap[key];\n },\n remove: (key) => {\n return isBrowser() ? localStorage.removeItem(key) : delete storageMap[key];\n }\n};\nexport const isBrowser = () => {\n return !(typeof process === 'object' && String(process) === '[object process]');\n}","import { OidcClient } from 'oidc-client-ts';\nimport { storage } from './utils';\nimport jwt_decode from 'jwt-decode';\n\nconst DEFAULT_AUTH_URL = 'https://auth.gameglue.gg/realms/GameGlue';\n\n// Track if callback is being processed (prevents double-processing)\nlet _callbackPromise = null;\n\nexport class GameGlueAuth {\n constructor(cfg) {\n const authority = cfg.authUrl || DEFAULT_AUTH_URL;\n this._oidcSettings = {\n authority,\n client_id: cfg.clientId,\n redirect_uri: removeTrailingSlashes(cfg.redirect_uri || window.location.href),\n post_logout_redirect_uri: removeTrailingSlashes(window.location.href),\n response_type: \"code\",\n scope: `openid ${(cfg.scopes || []).join(' ')}`,\n response_mode: \"fragment\",\n filterProtocolClaims: true\n };\n this._oidcClient = new OidcClient(this._oidcSettings);\n this._refreshCallback = () => {};\n this._refreshTimeout = null;\n }\n\n /**\n * Check if user is authenticated.\n * If OAuth callback params are in URL, processes them first.\n * Safe to call multiple times - idempotent.\n * @returns {Promise<boolean>}\n */\n async isAuthenticated() {\n // If callback params present, process them first\n if (this._hasCallbackParams()) {\n await this._processCallback();\n }\n\n // Check for valid tokens\n return this._hasValidTokens();\n }\n\n /**\n * Redirect to OAuth login page.\n * Does not return - navigates away.\n */\n login() {\n this._oidcClient.createSigninRequest({ state: { bar: 15 } }).then((req) => {\n window.location = req.url;\n }).catch((err) => {\n console.error('Failed to create signin request:', err);\n });\n }\n\n /**\n * Log out the user.\n * Clears local tokens and optionally redirects to Keycloak logout.\n * @param {Object} options - { redirect?: boolean }\n */\n logout(options = {}) {\n // Clear local tokens\n storage.remove('gg-auth-token');\n storage.remove('gg-refresh-token');\n clearTimeout(this._refreshTimeout);\n\n // Optionally redirect to Keycloak logout\n if (options.redirect !== false) {\n const logoutUrl = `${this._oidcSettings.authority}/protocol/openid-connect/logout?post_logout_redirect_uri=${encodeURIComponent(this._oidcSettings.post_logout_redirect_uri)}`;\n window.location.href = logoutUrl;\n }\n }\n\n /**\n * Get the current user's ID.\n * @throws {Error} if not authenticated\n * @returns {string}\n */\n getUser() {\n const token = this._getAccessToken();\n if (!token) {\n throw new Error('Not authenticated');\n }\n const decoded = jwt_decode(token);\n return decoded.sub;\n }\n\n /**\n * Get the access token for API calls.\n * @returns {string|null}\n */\n getAccessToken() {\n return this._getAccessToken();\n }\n\n /**\n * Register callback for token refresh events.\n * @param {Function} callback\n */\n onTokenRefreshed(callback) {\n this._refreshCallback = callback;\n }\n\n // ============ Internal Methods ============\n\n _hasCallbackParams() {\n return location.hash.includes(\"state=\") &&\n (location.hash.includes(\"code=\") || location.hash.includes(\"error=\"));\n }\n\n _clearCallbackUrl() {\n window.history.replaceState(\"\", document.title, window.location.pathname + window.location.search);\n }\n\n async _processCallback() {\n // If already processing, wait for that to complete\n if (_callbackPromise) {\n await _callbackPromise;\n return;\n }\n\n // Start processing\n _callbackPromise = this._doProcessCallback();\n\n try {\n await _callbackPromise;\n } finally {\n _callbackPromise = null;\n }\n }\n\n async _doProcessCallback() {\n try {\n const response = await this._oidcClient.processSigninResponse(window.location.href);\n\n if (response.error) {\n this._clearCallbackUrl();\n throw new Error(response.error);\n }\n\n if (!response.access_token) {\n this._clearCallbackUrl();\n throw new Error('No access token received');\n }\n\n this._setAccessToken(response.access_token);\n this._setRefreshToken(response.refresh_token);\n this._clearCallbackUrl();\n } catch (err) {\n // If we failed but tokens exist (another call succeeded), that's fine\n if (this._hasValidTokens()) {\n this._clearCallbackUrl();\n return;\n }\n this._clearCallbackUrl();\n throw err;\n }\n }\n\n _hasValidTokens() {\n const token = this._getAccessToken();\n if (!token) {\n return false;\n }\n\n try {\n const decoded = jwt_decode(token);\n const expirationDate = new Date(decoded.exp * 1000);\n return expirationDate > new Date();\n } catch {\n return false;\n }\n }\n\n _getAccessToken() {\n const token = storage.get('gg-auth-token');\n if (token) {\n this._setTokenRefreshTimeout(token);\n }\n return token;\n }\n\n _setAccessToken(token) {\n this._setTokenRefreshTimeout(token);\n return storage.set('gg-auth-token', token);\n }\n\n _setRefreshToken(token) {\n return storage.set('gg-refresh-token', token);\n }\n\n _getRefreshToken() {\n return storage.get('gg-refresh-token');\n }\n\n _setTokenRefreshTimeout(token) {\n if (!token) return;\n\n clearTimeout(this._refreshTimeout);\n\n try {\n const timeUntilExp = (jwt_decode(token).exp * 1000) - Date.now() - 5000;\n if (timeUntilExp > 0) {\n this._refreshTimeout = setTimeout(() => {\n this._attemptRefresh();\n }, timeUntilExp);\n }\n } catch {\n // Invalid token, ignore\n }\n }\n\n async _attemptRefresh() {\n const url = `${this._oidcSettings.authority}/protocol/openid-connect/token`;\n const client_id = this._oidcSettings.client_id;\n const refresh_token = this._getRefreshToken();\n const grant_type = 'refresh_token';\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded'\n },\n body: new URLSearchParams({\n client_id,\n grant_type,\n refresh_token\n })\n });\n\n if (response.status === 200) {\n const resObj = await response.json();\n this._setAccessToken(resObj.access_token);\n this._setRefreshToken(resObj.refresh_token);\n this._refreshCallback(resObj.access_token);\n }\n } catch (e) {\n console.error('Token refresh failed:', e);\n }\n }\n}\n\nfunction removeTrailingSlashes(url) {\n if (url.endsWith('/')) {\n return url.replace(/\\/+$/, '');\n }\n return url;\n}\n","import EventEmitter from 'event-emitter';\n\nexport class Listener {\n constructor(socket, config) {\n this._config = config;\n this._socket = socket;\n this._callbacks = [];\n this._fields = config.fields ? [...config.fields] : null;\n }\n\n async establishConnection() {\n if (!this._socket || !this._config.userId || !this._config.gameId) {\n throw new Error('Missing arguments in establishConnection');\n }\n return new Promise((resolve) => {\n // Use object format if fields are specified, otherwise use legacy string format\n let listenPayload;\n if (this._fields) {\n listenPayload = {\n userId: this._config.userId,\n gameId: this._config.gameId,\n fields: this._fields\n };\n } else {\n listenPayload = `${this._config.userId}:${this._config.gameId}`;\n }\n\n this._socket.timeout(5000).emit('listen', listenPayload, (error, response) => {\n if (error) {\n return resolve({status: 'failed', reason: 'Listen request timed out.'});\n }\n if (response.status === 'success') {\n return resolve({status: 'success'});\n } else {\n return resolve({status: 'failed', reason: response.reason});\n }\n });\n });\n }\n\n setupEventListener() {\n this._socket.on('update', (payload) => {\n // Apply client-side field filtering if fields are specified\n if (this._fields && this._fields.length > 0 && payload?.data) {\n const filteredData = {};\n for (const field of this._fields) {\n if (field in payload.data) {\n filteredData[field] = payload.data[field];\n }\n }\n this.emit('update', { ...payload, data: filteredData });\n } else {\n // No filtering - pass through full payload\n this.emit('update', payload);\n }\n });\n return this;\n }\n\n /**\n * Subscribe to additional fields dynamically\n * @param {string[]} fields - Array of field names to add\n * @returns {Promise<{status: string, reason?: string}>}\n */\n async subscribe(fields) {\n if (!Array.isArray(fields) || fields.length === 0) {\n throw new Error('fields must be a non-empty array');\n }\n\n // Add new fields to existing list\n if (!this._fields) {\n this._fields = [...fields];\n } else {\n for (const field of fields) {\n if (!this._fields.includes(field)) {\n this._fields.push(field);\n }\n }\n }\n\n return this._updateSubscription();\n }\n\n /**\n * Unsubscribe from specific fields\n * @param {string[]} fields - Array of field names to remove\n * @returns {Promise<{status: string, reason?: string}>}\n */\n async unsubscribe(fields) {\n if (!Array.isArray(fields) || fields.length === 0) {\n throw new Error('fields must be a non-empty array');\n }\n\n if (!this._fields) {\n // Currently receiving all fields, create explicit list without these fields\n throw new Error('Cannot unsubscribe when receiving all fields. Use subscribe() first to set explicit field list.');\n }\n\n this._fields = this._fields.filter(f => !fields.includes(f));\n\n return this._updateSubscription();\n }\n\n /**\n * Get the current list of subscribed fields\n * @returns {string[]|null} - Array of field names, or null if receiving all fields\n */\n getFields() {\n return this._fields ? [...this._fields] : null;\n }\n\n /**\n * Send a command to the broadcaster (game client)\n * @param {string} field - The field/action name to set\n * @param {any} value - The value to set\n * @returns {Promise<{status: string, reason?: string}>}\n */\n async sendCommand(field, value) {\n if (!field || typeof field !== 'string') {\n throw new Error('field must be a non-empty string');\n }\n\n return new Promise((resolve) => {\n const payload = {\n userId: this._config.userId,\n gameId: this._config.gameId,\n data: {\n fieldName: field,\n value\n }\n };\n\n this._socket.timeout(5000).emit('set', payload, (error, response) => {\n if (error) {\n return resolve({ status: 'failed', reason: 'Command request timed out.' });\n }\n return resolve(response);\n });\n });\n }\n\n /**\n * Internal method to send subscription update to server\n */\n async _updateSubscription() {\n return new Promise((resolve) => {\n const payload = {\n userId: this._config.userId,\n gameId: this._config.gameId,\n fields: this._fields\n };\n\n this._socket.timeout(5000).emit('listen-update', payload, (error, response) => {\n if (error) {\n return resolve({status: 'failed', reason: 'Update request timed out.'});\n }\n return resolve(response);\n });\n });\n }\n}\n\nEventEmitter(Listener.prototype);","import { GameGlueAuth } from './auth';\nimport { io } from \"socket.io-client\";\nimport { Listener } from \"./listener\";\n\nconst GAME_IDS = {\n 'msfs': true,\n};\n\nconst DEFAULT_SOCKET_URL = 'https://socks.gameglue.gg';\n\nclass GameGlue extends GameGlueAuth {\n constructor(cfg) {\n super(cfg);\n this._socket = null;\n this._socketUrl = cfg.socketUrl || DEFAULT_SOCKET_URL;\n this._connectPromise = null;\n }\n\n /**\n * Create a listener for game telemetry.\n * Connects to socket server lazily on first call.\n * @param {Object} config - { userId, gameId, fields? }\n * @returns {Promise<Listener>}\n */\n async createListener(config) {\n if (!config) throw new Error('Not a valid listener config');\n if (!config.gameId || !GAME_IDS[config.gameId]) throw new Error('Not a valid Game ID');\n if (!config.userId) throw new Error('User ID not supplied');\n if (config.fields && !Array.isArray(config.fields)) throw new Error('fields must be an array');\n\n // Ensure socket is connected (lazy initialization)\n await this._ensureConnected();\n\n const listener = new Listener(this._socket, config);\n const establishConnectionResponse = await listener.establishConnection();\n\n // Handle reconnection\n this._socket.io.on('reconnect_attempt', () => {\n this._updateSocketAuth(this.getAccessToken());\n });\n this._socket.io.on('reconnect', () => {\n listener.establishConnection();\n });\n\n if (establishConnectionResponse.status !== 'success') {\n throw new Error(`There was a problem setting up the listener. Reason: ${establishConnectionResponse.reason}`);\n }\n\n return listener.setupEventListener();\n }\n\n // ============ Internal Methods ============\n\n async _ensureConnected() {\n // Already connected\n if (this._socket?.connected) {\n return;\n }\n\n // Connection in progress - wait for it\n if (this._connectPromise) {\n await this._connectPromise;\n return;\n }\n\n // Start new connection\n this._connectPromise = this._connect();\n\n try {\n await this._connectPromise;\n } finally {\n this._connectPromise = null;\n }\n }\n\n _connect() {\n return new Promise((resolve, reject) => {\n const token = this.getAccessToken();\n\n if (!token) {\n reject(new Error('Not authenticated - call isAuthenticated() first'));\n return;\n }\n\n this._socket = io(this._socketUrl, {\n transports: ['websocket'],\n auth: { token }\n });\n\n this._socket.on('connect', () => {\n resolve();\n });\n\n this._socket.on('connect_error', (err) => {\n reject(new Error(`Socket connection failed: ${err.message}`));\n });\n\n // Update socket auth when token refreshes\n this.onTokenRefreshed((newToken) => {\n this._updateSocketAuth(newToken);\n });\n });\n }\n\n _updateSocketAuth(authToken) {\n if (this._socket) {\n this._socket.auth.token = authToken;\n }\n }\n}\n\nif (typeof window !== 'undefined') {\n window.GameGlue = GameGlue;\n}\n\nexport default GameGlue;\nexport { GameGlue };\n"],"names":["storageMap","storage","key","value","isBrowser","localStorage","setItem","getItem","removeItem","process","String","_callbackPromise","GameGlueAuth","constructor","cfg","authority","authUrl","this","_oidcSettings","client_id","clientId","redirect_uri","removeTrailingSlashes","window","location","href","post_logout_redirect_uri","response_type","scope","scopes","join","response_mode","filterProtocolClaims","_oidcClient","OidcClient","_refreshCallback","_refreshTimeout","isAuthenticated","_hasCallbackParams","_processCallback","_hasValidTokens","login","createSigninRequest","state","bar","then","req","url","catch","err","console","error","logout","options","clearTimeout","redirect","logoutUrl","encodeURIComponent","getUser","token","_getAccessToken","Error","jwt_decode","sub","getAccessToken","onTokenRefreshed","callback","hash","includes","_clearCallbackUrl","history","replaceState","document","title","pathname","search","_doProcessCallback","response","processSigninResponse","access_token","_setAccessToken","_setRefreshToken","refresh_token","decoded","Date","exp","_setTokenRefreshTimeout","_getRefreshToken","timeUntilExp","now","setTimeout","_attemptRefresh","fetch","method","headers","body","URLSearchParams","grant_type","status","resObj","json","e","endsWith","replace","Listener","socket","config","_config","_socket","_callbacks","_fields","fields","establishConnection","userId","gameId","Promise","resolve","listenPayload","timeout","emit","reason","setupEventListener","on","payload","length","data","filteredData","field","subscribe","Array","isArray","push","_updateSubscription","unsubscribe","filter","f","getFields","sendCommand","fieldName","EventEmitter","prototype","GAME_IDS","msfs","GameGlue","super","_socketUrl","socketUrl","_connectPromise","createListener","_ensureConnected","listener","establishConnectionResponse","io","_updateSocketAuth","connected","_connect","reject","transports","auth","message","newToken","authToken"],"mappings":"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"}