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 +79 -16
- package/dist/gg.cjs.js +1 -1
- package/dist/gg.cjs.js.map +1 -1
- package/dist/gg.esm.js +1 -1
- package/dist/gg.esm.js.map +1 -1
- package/dist/gg.umd.js +1 -1
- package/dist/gg.umd.js.map +1 -1
- package/examples/flight-dashboard.html +69 -39
- package/package.json +1 -1
- package/src/auth.js +187 -86
- package/src/auth.spec.js +167 -77
- package/src/index.js +82 -44
- package/src/listener.js +16 -2
- package/src/listener.spec.js +139 -0
- package/src/test/setup.js +2 -1
- package/src/utils.js +3 -0
- package/src/utils.spec.js +14 -0
- package/dist/gg.sdk.js +0 -1
- /package/{babel.config.js → babel.config.cjs} +0 -0
- /package/{jest.config.js → jest.config.cjs} +0 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
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
|
|
21
|
+
npm i gameglue
|
|
22
22
|
```
|
|
23
23
|
##### Yarn
|
|
24
24
|
```shell
|
|
25
|
-
yarn add
|
|
25
|
+
yarn add gameglue
|
|
26
26
|
```
|
|
27
27
|
##### Script tag
|
|
28
28
|
```html
|
|
29
|
-
<script src="https://unpkg.com/
|
|
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
|
|
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
|
-
###
|
|
42
|
+
### Check authentication and login
|
|
43
|
+
|
|
43
44
|
```javascript
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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
|
|
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
|
-
| `
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
| `
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
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
|
package/dist/gg.cjs.js.map
CHANGED
|
@@ -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":"0JAAA,MAAMA,EAAa,CAAA,EACNC,EACN,CAACC,EAAKC,IACFC,IAAcC,aAAaC,QAAQJ,EAAKC,GAAUH,EAAWE,GAAOC,EAFlEF,EAILC,GACGE,IAAcC,aAAaE,QAAQL,GAAOF,EAAWE,GAGnDE,EAAY,MACK,iBAAZI,SAA4C,qBAApBC,OAAOD,UCL1C,MAAME,EACX,WAAAC,CAAYC,GACVC,KAAKC,cAAgB,CACnBC,UAAW,2CACXC,UAAWJ,EAAIK,SACfC,aAAcC,EAAsBP,EAAIM,cAAgBE,OAAOC,SAASC,MACxEC,yBAA0BJ,EAAsBC,OAAOC,SAASC,MAChEE,cAAe,OACfC,MAAO,WAAWb,EAAIc,QAAQ,IAAIC,KAAK,OACvCC,cAAe,WACfC,sBAAsB,GAExBhB,KAAKiB,YAAc,IAAIC,aAAWlB,KAAKC,eACvCD,KAAKmB,iBAAmB,OACxBnB,KAAKoB,gBAAkB,IACzB,CACA,sBAAAC,CAAuBC,GACrB,IAAKA,EACH,OAEFC,aAAavB,KAAKoB,iBAClB,MAAMI,EAAwC,IAAxBC,EAAWH,GAAOI,IAAcC,KAAKC,MAAQ,IAC/DJ,EAAe,IACjBxB,KAAKoB,gBAAkBS,WAAW,KAChC7B,KAAK8B,kBACJN,GAEP,CACA,cAAAO,CAAeT,GAEb,OADAtB,KAAKqB,uBAAuBC,GACrBlC,EAAY,gBAAiBkC,EACtC,CACA,cAAAU,GACE,IAAIV,EAAQlC,EAAY,iBAExB,OADAY,KAAKqB,uBAAuBC,GACrBA,CACT,CACA,SAAAW,GAEE,OADgBR,EAAWzB,KAAKgC,kBACjBE,GACjB,CACA,eAAAC,CAAgBb,GACd,OAAOlC,EAAY,mBAAoBkC,EACzC,CACA,eAAAc,CAAgBd,GACd,OAAOlC,EAAY,mBACrB,CACA,6BAAAiD,GACE,OAAQ7B,SAAS8B,KAAKC,SAAS,YAAc/B,SAAS8B,KAAKC,SAAS,UAAY/B,SAAS8B,KAAKC,SAAS,UACzG,CACA,4BAAMC,GACJ,IAAIC,QAAiBzC,KAAKiB,YAAYyB,sBAAsBnC,OAAOC,SAASC,OACxEgC,EAASE,OAAUF,EAASG,cAIhCrC,OAAOsC,QAAQC,UAAU,GAAIC,SAASC,MAAOzC,OAAOC,SAASyC,SAAW1C,OAAOC,SAAS0C,QACxFlD,KAAK+B,eAAeU,EAASG,cAC7B5C,KAAKmC,gBAAgBM,EAASU,gBAL5BC,QAAQT,MAAMF,EAASE,MAM3B,CACA,gBAAAU,CAAiBC,GACftD,KAAKmB,iBAAmBmC,CAC1B,CACA,qBAAMC,CAAgBC,GAEpB,IAAIZ,EAAe5C,KAAKgC,iBAGxB,IAAKY,EACH,OAAO,EAGT,MAAMa,EAAUhC,EAAWmB,GAErBc,EADiB,IAAI/B,KAAiB,IAAZ8B,EAAQ/B,KACJ,IAAIC,KAExC,OAAI+B,IAAcF,SACVxD,KAAK8B,iBACJ9B,KAAKuD,iBAAgB,MAKrBG,GAAaF,EACxB,CACA,cAAAG,CAAerC,GACb,MAAMmC,EAAUhC,EAAWH,GAE3B,OADuB,IAAIK,KAAiB,IAAZ8B,EAAQ/B,KACf,IAAIC,IAC/B,CACA,oBAAMG,GACJ,MAAM8B,EAAM,GAAG5D,KAAKC,cAAcC,0CAC5BC,EAAYH,KAAKC,cAAcE,UAC/BgD,EAAgBnD,KAAKoC,kBAG3B,IACE,MAAMK,QAAkBoB,MAAMD,EAAK,CACjCE,OAAQ,OACRC,QAAQ,CACN,eAAgB,qCAElBC,KAAM,IAAIC,gBAAgB,CACxB9D,YACA+D,WAVa,gBAWbf,oBAGJ,GAAwB,MAApBV,EAAS0B,OAAgB,CAC3B,MAAMC,QAAe3B,EAAS4B,OAC9BrE,KAAK+B,eAAeqC,EAAOxB,cAC3B5C,KAAKmC,gBAAgBiC,EAAOjB,eAC5BnD,KAAKmB,iBAAiBiD,EACxB,CACF,CAAE,MAAME,GACNlB,QAAQmB,IAAI,UAAWD,EACzB,CACF,CACA,oBAAAE,GACExE,KAAKiB,YAAYwD,oBAAoB,CAAEC,MAAO,CAAEC,IAAK,MAAQC,KAAK,SAASC,GACzEtE,OAAOC,SAAWqE,EAAIjB,GACxB,GAAGkB,MAAM,SAASC,GAChB3B,QAAQT,MAAMoC,EAChB,EACF,CACA,kBAAMC,GACAhF,KAAKqC,uCACDrC,KAAKwC,+BAGexC,KAAKuD,yBAEzBvD,KAAKwE,sBAEf,EAGF,SAASlE,EAAsBsD,GAC7B,OAAIA,EAAIqB,SAAS,KACRrB,EAAIsB,QAAQ,OAAQ,IAEtBtB,CACT,CCnJA,MAAMuB,EAAeC,QAAQ,iBAEtB,MAAMC,EACX,WAAAvF,CAAYwF,EAAQC,GAClBvF,KAAKwF,QAAUD,EACfvF,KAAKyF,QAAUH,EACftF,KAAK0F,WAAa,GAClB1F,KAAK2F,QAAUJ,EAAOK,OAAS,IAAIL,EAAOK,QAAU,IACtD,CAEA,yBAAMC,GACJ,IAAK7F,KAAKyF,UAAYzF,KAAKwF,QAAQM,SAAW9F,KAAKwF,QAAQO,OACzD,MAAM,IAAIC,MAAM,4CAElB,OAAO,IAAIC,QAASC,IAElB,IAAIC,EAEFA,EADEnG,KAAK2F,QACS,CACdG,OAAQ9F,KAAKwF,QAAQM,OACrBC,OAAQ/F,KAAKwF,QAAQO,OACrBH,OAAQ5F,KAAK2F,SAGC,GAAG3F,KAAKwF,QAAQM,UAAU9F,KAAKwF,QAAQO,SAGzD/F,KAAKyF,QAAQW,QAAQ,KAAMC,KAAK,SAAUF,EAAe,CAACxD,EAAOF,IAC3DE,EACKuD,EAAQ,CAAC/B,OAAQ,SAAUmC,OAAQ,8BAEpB,YAApB7D,EAAS0B,OACJ+B,EAAQ,CAAC/B,OAAQ,YAEjB+B,EAAQ,CAAC/B,OAAQ,SAAUmC,OAAQ7D,EAAS6D,WAI3D,CAEA,kBAAAC,GAEE,OADAvG,KAAKyF,QAAQe,GAAG,SAAUxG,KAAKqG,KAAKI,KAAKzG,KAAM,WACxCA,IACT,CAOA,eAAM0G,CAAUd,GACd,IAAKe,MAAMC,QAAQhB,IAA6B,IAAlBA,EAAOiB,OACnC,MAAM,IAAIb,MAAM,oCAIlB,GAAKhG,KAAK2F,QAGR,IAAK,MAAMmB,KAASlB,EACb5F,KAAK2F,QAAQpD,SAASuE,IACzB9G,KAAK2F,QAAQoB,KAAKD,QAJtB9G,KAAK2F,QAAU,IAAIC,GASrB,OAAO5F,KAAKgH,qBACd,CAOA,iBAAMC,CAAYrB,GAChB,IAAKe,MAAMC,QAAQhB,IAA6B,IAAlBA,EAAOiB,OACnC,MAAM,IAAIb,MAAM,oCAGlB,IAAKhG,KAAK2F,QAER,MAAM,IAAIK,MAAM,mGAKlB,OAFAhG,KAAK2F,QAAU3F,KAAK2F,QAAQuB,OAAOC,IAAMvB,EAAOrD,SAAS4E,IAElDnH,KAAKgH,qBACd,CAMA,SAAAI,GACE,OAAOpH,KAAK2F,QAAU,IAAI3F,KAAK2F,SAAW,IAC5C,CAQA,iBAAM0B,CAAYP,EAAOxH,GACvB,IAAKwH,GAA0B,iBAAVA,EACnB,MAAM,IAAId,MAAM,oCAGlB,OAAO,IAAIC,QAASC,IAClB,MAAMoB,EAAU,CACdxB,OAAQ9F,KAAKwF,QAAQM,OACrBC,OAAQ/F,KAAKwF,QAAQO,OACrBwB,KAAM,CACJC,UAAWV,EACXxH,UAIJU,KAAKyF,QAAQW,QAAQ,KAAMC,KAAK,MAAOiB,EAAS,CAAC3E,EAAOF,IAE7CyD,EADLvD,EACa,CAAEwB,OAAQ,SAAUmC,OAAQ,8BAE9B7D,KAGrB,CAKA,yBAAMuE,GACJ,OAAO,IAAIf,QAASC,IAClB,MAAMoB,EAAU,CACdxB,OAAQ9F,KAAKwF,QAAQM,OACrBC,OAAQ/F,KAAKwF,QAAQO,OACrBH,OAAQ5F,KAAK2F,SAGf3F,KAAKyF,QAAQW,QAAQ,KAAMC,KAAK,gBAAiBiB,EAAS,CAAC3E,EAAOF,IAEvDyD,EADLvD,EACa,CAACwB,OAAQ,SAAUmC,OAAQ,6BAE7B7D,KAGrB,EAGF0C,EAAaE,EAASoC,WChJtB,MAAMC,EAAW,CACfC,MAAQ,GAGV,MAAMC,UAAiB/H,EACrB,WAAAC,CAAYC,GACV8H,MAAM9H,GACNC,KAAKyF,SAAU,CACjB,CAEA,UAAMqC,GAKJ,aAJM9H,KAAKgF,qBACDhF,KAAKuD,yBACPvD,KAAK+H,aAEN/H,KAAKiC,WACd,CAEA,gBAAM8F,GACJ,OAAO,IAAI9B,QAASC,IAClB,MAAM5E,EAAQtB,KAAKgC,iBAEnBhC,KAAKyF,QAAUuC,EAAAA,GAAG,4BAA6B,CAC7CC,WAAY,CAAC,aACbH,KAAM,CACJxG,WAIJtB,KAAKyF,QAAQe,GAAG,UAAW,KACzBN,MAEFlG,KAAKqD,iBAAiBrD,KAAKkI,mBAE/B,CAEA,gBAAAA,CAAiBC,GACfnI,KAAKyF,QAAQqC,KAAKxG,MAAQ6G,CAC5B,CAEA,oBAAMC,CAAe7C,GACnB,IAAKA,EAAQ,MAAM,IAAIS,MAAM,+BAC7B,IAAKT,EAAOQ,SAAW2B,EAASnC,EAAOQ,QAAS,MAAM,IAAIC,MAAM,uBAChE,IAAKT,EAAOO,OAAQ,MAAM,IAAIE,MAAM,wBACpC,GAAIT,EAAOK,SAAWe,MAAMC,QAAQrB,EAAOK,QAAS,MAAM,IAAII,MAAM,2BAG/DhG,KAAKyF,eACFzF,KAAK+H,aAGb,MAAMM,EAAW,IAAIhD,EAASrF,KAAKyF,QAASF,GACtC+C,QAAoCD,EAASxC,sBASnD,GARA7F,KAAKyF,QAAQuC,GAAGxB,GAAG,oBAAsB+B,IACvCnF,QAAQmB,IAAI,mBACZvE,KAAKkI,iBAAiBlI,KAAKgC,oBAE7BhC,KAAKyF,QAAQuC,GAAGxB,GAAG,YAAa,KAC9B6B,EAASxC,wBAGgC,YAAvCyC,EAA4BnE,OAC9B,MAAM,IAAI6B,MAAM,wDAAwDsC,EAA4BhC,UAGtG,OAAO+B,EAAS9B,oBAClB,EAGoB,oBAAXhG,SACTA,OAAOqH,SAAWA"}
|
|
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
|
|
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
|
package/dist/gg.esm.js.map
CHANGED
|
@@ -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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"}
|