gameglue 4.0.1 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +275 -275
  3. package/babel.config.cjs +5 -5
  4. package/coverage/auth.js.html +525 -525
  5. package/coverage/base.css +224 -224
  6. package/coverage/block-navigation.js +87 -87
  7. package/coverage/favicon.png +0 -0
  8. package/coverage/index.html +175 -175
  9. package/coverage/index.js.html +309 -309
  10. package/coverage/lcov-report/auth.js.html +525 -525
  11. package/coverage/lcov-report/base.css +224 -224
  12. package/coverage/lcov-report/block-navigation.js +87 -87
  13. package/coverage/lcov-report/favicon.png +0 -0
  14. package/coverage/lcov-report/index.html +175 -175
  15. package/coverage/lcov-report/index.js.html +309 -309
  16. package/coverage/lcov-report/listener.js.html +528 -528
  17. package/coverage/lcov-report/prettify.css +1 -1
  18. package/coverage/lcov-report/prettify.js +2 -2
  19. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  20. package/coverage/lcov-report/sorter.js +210 -210
  21. package/coverage/lcov-report/user.js.html +117 -117
  22. package/coverage/lcov-report/utils.js.html +117 -117
  23. package/coverage/lcov.info +391 -391
  24. package/coverage/listener.js.html +528 -528
  25. package/coverage/prettify.css +1 -1
  26. package/coverage/prettify.js +2 -2
  27. package/coverage/sort-arrow-sprite.png +0 -0
  28. package/coverage/sorter.js +210 -210
  29. package/coverage/user.js.html +117 -117
  30. package/coverage/utils.js.html +117 -117
  31. package/dist/gg.cjs.js +1 -1
  32. package/dist/gg.cjs.js.map +1 -1
  33. package/dist/gg.esm.js +1 -1
  34. package/dist/gg.esm.js.map +1 -1
  35. package/dist/gg.umd.js +1 -1
  36. package/dist/gg.umd.js.map +1 -1
  37. package/examples/certs/cert.pem +19 -19
  38. package/examples/certs/key.pem +28 -28
  39. package/examples/flight-dashboard.html +431 -431
  40. package/examples/server.js +99 -99
  41. package/examples/telemetry-validator.html +1410 -1410
  42. package/jest.config.cjs +33 -33
  43. package/package.json +56 -56
  44. package/rollup.config.js +57 -57
  45. package/src/auth.js +255 -255
  46. package/src/auth.spec.js +481 -481
  47. package/src/index.js +168 -168
  48. package/src/listener.js +196 -196
  49. package/src/listener.spec.js +598 -598
  50. package/src/presence_listener.js +112 -112
  51. package/src/test/fixtures.js +106 -106
  52. package/src/test/setup.js +51 -51
  53. package/src/utils.js +63 -63
  54. package/src/utils.spec.js +78 -78
  55. package/types/index.d.ts +338 -338
  56. package/webpack.config.js +15 -15
@@ -1,112 +1,112 @@
1
- import EventEmitter from 'event-emitter';
2
- import { getGameSchema, normalizeTelemetry } from '@gameglue/schemas';
3
-
4
- export class PresenceListener {
5
- constructor(socket, config) {
6
- this._config = config;
7
- this._socket = socket;
8
- this._gameSchema = getGameSchema(config.gameId);
9
- this._broadcasters = [];
10
- }
11
-
12
- async establishConnection() {
13
- if (!this._socket || !this._config.clientId || !this._config.gameId) {
14
- throw new Error('Missing arguments in establishConnection');
15
- }
16
-
17
- return new Promise((resolve) => {
18
- const payload = {
19
- clientId: this._config.clientId,
20
- gameId: this._config.gameId
21
- };
22
-
23
- this._socket.timeout(5000).emit('presenceSubscribe', payload, (error, response) => {
24
- if (error) {
25
- return resolve({ status: 'failed', reason: 'Presence request timed out.' });
26
- }
27
-
28
- if (response?.status === 'success') {
29
- const broadcasters = Array.isArray(response.broadcasters)
30
- ? response.broadcasters.map((snapshot) => this._normalizeSnapshot(snapshot))
31
- : [];
32
- this._broadcasters = broadcasters;
33
- return resolve({ status: 'success', broadcasters });
34
- }
35
-
36
- return resolve({
37
- status: 'failed',
38
- reason: response?.reason || 'Presence subscription failed.'
39
- });
40
- });
41
- });
42
- }
43
-
44
- setupEventListener() {
45
- this._socket.on('presence-update', (snapshot) => {
46
- const normalized = this._normalizeSnapshot(snapshot);
47
- this._upsertBroadcaster(normalized);
48
- this.emit('update', normalized);
49
- });
50
-
51
- this._socket.on('presence-remove', ({ userId }) => {
52
- if (!userId) return;
53
- this._broadcasters = this._broadcasters.filter((b) => b.userId !== userId);
54
- this.emit('remove', { userId });
55
- });
56
-
57
- this._socket.on('connect', () => {
58
- this.emit('connect');
59
- });
60
-
61
- this._socket.on('disconnect', (reason) => {
62
- this.emit('disconnect', reason);
63
- });
64
-
65
- return this;
66
- }
67
-
68
- getBroadcasters() {
69
- return [...this._broadcasters];
70
- }
71
-
72
- disconnect() {
73
- if (this._socket) {
74
- this._socket.disconnect();
75
- }
76
- }
77
-
78
- _normalizeSnapshot(snapshot) {
79
- if (!snapshot || typeof snapshot !== 'object') {
80
- return snapshot;
81
- }
82
-
83
- const rawData = snapshot.data;
84
- const normalizedData = rawData && this._gameSchema
85
- ? normalizeTelemetry(rawData, this._gameSchema)
86
- : rawData;
87
-
88
- return {
89
- ...snapshot,
90
- data: normalizedData,
91
- raw: rawData
92
- };
93
- }
94
-
95
- _upsertBroadcaster(snapshot) {
96
- if (!snapshot || !snapshot.userId) {
97
- return;
98
- }
99
-
100
- const idx = this._broadcasters.findIndex((b) => b.userId === snapshot.userId);
101
- if (idx >= 0) {
102
- const next = [...this._broadcasters];
103
- next[idx] = snapshot;
104
- this._broadcasters = next;
105
- return;
106
- }
107
-
108
- this._broadcasters = [...this._broadcasters, snapshot];
109
- }
110
- }
111
-
112
- EventEmitter(PresenceListener.prototype);
1
+ import EventEmitter from 'event-emitter';
2
+ import { getGameSchema, normalizeTelemetry } from '@gameglue/schemas';
3
+
4
+ export class PresenceListener {
5
+ constructor(socket, config) {
6
+ this._config = config;
7
+ this._socket = socket;
8
+ this._gameSchema = getGameSchema(config.gameId);
9
+ this._broadcasters = [];
10
+ }
11
+
12
+ async establishConnection() {
13
+ if (!this._socket || !this._config.clientId || !this._config.gameId) {
14
+ throw new Error('Missing arguments in establishConnection');
15
+ }
16
+
17
+ return new Promise((resolve) => {
18
+ const payload = {
19
+ clientId: this._config.clientId,
20
+ gameId: this._config.gameId
21
+ };
22
+
23
+ this._socket.timeout(5000).emit('presenceSubscribe', payload, (error, response) => {
24
+ if (error) {
25
+ return resolve({ status: 'failed', reason: 'Presence request timed out.' });
26
+ }
27
+
28
+ if (response?.status === 'success') {
29
+ const broadcasters = Array.isArray(response.broadcasters)
30
+ ? response.broadcasters.map((snapshot) => this._normalizeSnapshot(snapshot))
31
+ : [];
32
+ this._broadcasters = broadcasters;
33
+ return resolve({ status: 'success', broadcasters });
34
+ }
35
+
36
+ return resolve({
37
+ status: 'failed',
38
+ reason: response?.reason || 'Presence subscription failed.'
39
+ });
40
+ });
41
+ });
42
+ }
43
+
44
+ setupEventListener() {
45
+ this._socket.on('presence-update', (snapshot) => {
46
+ const normalized = this._normalizeSnapshot(snapshot);
47
+ this._upsertBroadcaster(normalized);
48
+ this.emit('update', normalized);
49
+ });
50
+
51
+ this._socket.on('presence-remove', ({ userId }) => {
52
+ if (!userId) return;
53
+ this._broadcasters = this._broadcasters.filter((b) => b.userId !== userId);
54
+ this.emit('remove', { userId });
55
+ });
56
+
57
+ this._socket.on('connect', () => {
58
+ this.emit('connect');
59
+ });
60
+
61
+ this._socket.on('disconnect', (reason) => {
62
+ this.emit('disconnect', reason);
63
+ });
64
+
65
+ return this;
66
+ }
67
+
68
+ getBroadcasters() {
69
+ return [...this._broadcasters];
70
+ }
71
+
72
+ disconnect() {
73
+ if (this._socket) {
74
+ this._socket.disconnect();
75
+ }
76
+ }
77
+
78
+ _normalizeSnapshot(snapshot) {
79
+ if (!snapshot || typeof snapshot !== 'object') {
80
+ return snapshot;
81
+ }
82
+
83
+ const rawData = snapshot.data;
84
+ const normalizedData = rawData && this._gameSchema
85
+ ? normalizeTelemetry(rawData, this._gameSchema)
86
+ : rawData;
87
+
88
+ return {
89
+ ...snapshot,
90
+ data: normalizedData,
91
+ raw: rawData
92
+ };
93
+ }
94
+
95
+ _upsertBroadcaster(snapshot) {
96
+ if (!snapshot || !snapshot.userId) {
97
+ return;
98
+ }
99
+
100
+ const idx = this._broadcasters.findIndex((b) => b.userId === snapshot.userId);
101
+ if (idx >= 0) {
102
+ const next = [...this._broadcasters];
103
+ next[idx] = snapshot;
104
+ this._broadcasters = next;
105
+ return;
106
+ }
107
+
108
+ this._broadcasters = [...this._broadcasters, snapshot];
109
+ }
110
+ }
111
+
112
+ EventEmitter(PresenceListener.prototype);
@@ -1,106 +1,106 @@
1
- // Test fixtures for GameGlueJS SDK
2
-
3
- // Helper to create a JWT with custom payload
4
- function createMockJwt(payload, expiresInSeconds = 3600) {
5
- const header = { alg: 'RS256', typ: 'JWT' };
6
- const now = Math.floor(Date.now() / 1000);
7
- const defaultPayload = {
8
- sub: 'user-123',
9
- exp: now + expiresInSeconds,
10
- iat: now,
11
- aud: 'gameglue-client',
12
- iss: 'https://auth.gameglue.gg/realms/GameGlue',
13
- scope: 'openid gg:broadcast msfs:read',
14
- ...payload
15
- };
16
-
17
- const base64Header = Buffer.from(JSON.stringify(header)).toString('base64url');
18
- const base64Payload = Buffer.from(JSON.stringify(defaultPayload)).toString('base64url');
19
- // Fake signature (not cryptographically valid, but works for decoding tests)
20
- const signature = 'fake_signature_for_testing';
21
-
22
- return `${base64Header}.${base64Payload}.${signature}`;
23
- }
24
-
25
- // Valid access token (expires in 1 hour)
26
- const validAccessToken = createMockJwt({ sub: 'user-123' }, 3600);
27
-
28
- // Expired access token
29
- const expiredAccessToken = createMockJwt({ sub: 'user-123' }, -100);
30
-
31
- // Valid refresh token
32
- const validRefreshToken = createMockJwt({ sub: 'user-123', type: 'refresh' }, 86400);
33
-
34
- // Mock user configuration
35
- const mockConfig = {
36
- clientId: 'test-client-id',
37
- redirect_uri: 'http://localhost:3000/callback',
38
- scopes: ['msfs:read', 'gg:broadcast']
39
- };
40
-
41
- // Mock listener configuration
42
- const mockListenerConfig = {
43
- gameId: 'msfs',
44
- userId: 'user-123',
45
- fields: ['altitude', 'airspeed', 'heading']
46
- };
47
-
48
- // Mock socket instance
49
- function createMockSocket() {
50
- const eventHandlers = {};
51
- const ioEventHandlers = {};
52
-
53
- return {
54
- auth: { token: null },
55
- connected: true,
56
- on: jest.fn((event, handler) => {
57
- eventHandlers[event] = handler;
58
- }),
59
- emit: jest.fn((event, data, callback) => {
60
- // Simulate successful responses
61
- if (callback) {
62
- if (event === 'listen') {
63
- callback(null, { status: 'success' });
64
- } else if (event === 'listen-update') {
65
- callback(null, { status: 'success' });
66
- } else if (event === 'set') {
67
- callback(null, { status: 'success' });
68
- } else {
69
- callback(null, { status: 'success' });
70
- }
71
- }
72
- }),
73
- timeout: jest.fn(function() {
74
- return this;
75
- }),
76
- disconnect: jest.fn(),
77
- io: {
78
- on: jest.fn((event, handler) => {
79
- ioEventHandlers[event] = handler;
80
- })
81
- },
82
- // Helpers for tests to trigger events
83
- _trigger: (event, data) => {
84
- if (eventHandlers[event]) {
85
- eventHandlers[event](data);
86
- }
87
- },
88
- _triggerIo: (event, data) => {
89
- if (ioEventHandlers[event]) {
90
- ioEventHandlers[event](data);
91
- }
92
- },
93
- _getHandler: (event) => eventHandlers[event],
94
- _getIoHandler: (event) => ioEventHandlers[event]
95
- };
96
- }
97
-
98
- module.exports = {
99
- createMockJwt,
100
- validAccessToken,
101
- expiredAccessToken,
102
- validRefreshToken,
103
- mockConfig,
104
- mockListenerConfig,
105
- createMockSocket
106
- };
1
+ // Test fixtures for GameGlueJS SDK
2
+
3
+ // Helper to create a JWT with custom payload
4
+ function createMockJwt(payload, expiresInSeconds = 3600) {
5
+ const header = { alg: 'RS256', typ: 'JWT' };
6
+ const now = Math.floor(Date.now() / 1000);
7
+ const defaultPayload = {
8
+ sub: 'user-123',
9
+ exp: now + expiresInSeconds,
10
+ iat: now,
11
+ aud: 'gameglue-client',
12
+ iss: 'https://auth.gameglue.gg/realms/GameGlue',
13
+ scope: 'openid gg:broadcast msfs:read',
14
+ ...payload
15
+ };
16
+
17
+ const base64Header = Buffer.from(JSON.stringify(header)).toString('base64url');
18
+ const base64Payload = Buffer.from(JSON.stringify(defaultPayload)).toString('base64url');
19
+ // Fake signature (not cryptographically valid, but works for decoding tests)
20
+ const signature = 'fake_signature_for_testing';
21
+
22
+ return `${base64Header}.${base64Payload}.${signature}`;
23
+ }
24
+
25
+ // Valid access token (expires in 1 hour)
26
+ const validAccessToken = createMockJwt({ sub: 'user-123' }, 3600);
27
+
28
+ // Expired access token
29
+ const expiredAccessToken = createMockJwt({ sub: 'user-123' }, -100);
30
+
31
+ // Valid refresh token
32
+ const validRefreshToken = createMockJwt({ sub: 'user-123', type: 'refresh' }, 86400);
33
+
34
+ // Mock user configuration
35
+ const mockConfig = {
36
+ clientId: 'test-client-id',
37
+ redirect_uri: 'http://localhost:3000/callback',
38
+ scopes: ['msfs:read', 'gg:broadcast']
39
+ };
40
+
41
+ // Mock listener configuration
42
+ const mockListenerConfig = {
43
+ gameId: 'msfs',
44
+ userId: 'user-123',
45
+ fields: ['altitude', 'airspeed', 'heading']
46
+ };
47
+
48
+ // Mock socket instance
49
+ function createMockSocket() {
50
+ const eventHandlers = {};
51
+ const ioEventHandlers = {};
52
+
53
+ return {
54
+ auth: { token: null },
55
+ connected: true,
56
+ on: jest.fn((event, handler) => {
57
+ eventHandlers[event] = handler;
58
+ }),
59
+ emit: jest.fn((event, data, callback) => {
60
+ // Simulate successful responses
61
+ if (callback) {
62
+ if (event === 'listen') {
63
+ callback(null, { status: 'success' });
64
+ } else if (event === 'listen-update') {
65
+ callback(null, { status: 'success' });
66
+ } else if (event === 'set') {
67
+ callback(null, { status: 'success' });
68
+ } else {
69
+ callback(null, { status: 'success' });
70
+ }
71
+ }
72
+ }),
73
+ timeout: jest.fn(function() {
74
+ return this;
75
+ }),
76
+ disconnect: jest.fn(),
77
+ io: {
78
+ on: jest.fn((event, handler) => {
79
+ ioEventHandlers[event] = handler;
80
+ })
81
+ },
82
+ // Helpers for tests to trigger events
83
+ _trigger: (event, data) => {
84
+ if (eventHandlers[event]) {
85
+ eventHandlers[event](data);
86
+ }
87
+ },
88
+ _triggerIo: (event, data) => {
89
+ if (ioEventHandlers[event]) {
90
+ ioEventHandlers[event](data);
91
+ }
92
+ },
93
+ _getHandler: (event) => eventHandlers[event],
94
+ _getIoHandler: (event) => ioEventHandlers[event]
95
+ };
96
+ }
97
+
98
+ module.exports = {
99
+ createMockJwt,
100
+ validAccessToken,
101
+ expiredAccessToken,
102
+ validRefreshToken,
103
+ mockConfig,
104
+ mockListenerConfig,
105
+ createMockSocket
106
+ };
package/src/test/setup.js CHANGED
@@ -1,51 +1,51 @@
1
- // Jest setup file for GameGlueJS SDK
2
-
3
- // Create a proper localStorage mock with internal store
4
- let localStore = {};
5
-
6
- const localStorageMock = {
7
- getItem: jest.fn((key) => localStore[key] || null),
8
- setItem: jest.fn((key, value) => {
9
- localStore[key] = String(value);
10
- }),
11
- removeItem: jest.fn((key) => {
12
- delete localStore[key];
13
- }),
14
- clear: jest.fn(() => {
15
- localStore = {};
16
- }),
17
- get length() {
18
- return Object.keys(localStore).length;
19
- },
20
- key: jest.fn((i) => Object.keys(localStore)[i] || null)
21
- };
22
-
23
- // Override localStorage on window (jsdom provides window)
24
- Object.defineProperty(window, 'localStorage', {
25
- value: localStorageMock,
26
- writable: true
27
- });
28
-
29
- // Also set on global for Node environment
30
- Object.defineProperty(global, 'localStorage', {
31
- value: localStorageMock,
32
- writable: true
33
- });
34
-
35
- // Mock window.history.pushState and replaceState
36
- window.history.pushState = jest.fn();
37
- window.history.replaceState = jest.fn();
38
-
39
- // Reset mocks and store before each test
40
- beforeEach(() => {
41
- localStore = {};
42
- localStorageMock.getItem.mockClear();
43
- localStorageMock.setItem.mockClear();
44
- localStorageMock.removeItem.mockClear();
45
- localStorageMock.clear.mockClear();
46
-
47
- // Reset window.location.hash using the proper jsdom approach
48
- window.location.hash = '';
49
-
50
- jest.clearAllMocks();
51
- });
1
+ // Jest setup file for GameGlueJS SDK
2
+
3
+ // Create a proper localStorage mock with internal store
4
+ let localStore = {};
5
+
6
+ const localStorageMock = {
7
+ getItem: jest.fn((key) => localStore[key] || null),
8
+ setItem: jest.fn((key, value) => {
9
+ localStore[key] = String(value);
10
+ }),
11
+ removeItem: jest.fn((key) => {
12
+ delete localStore[key];
13
+ }),
14
+ clear: jest.fn(() => {
15
+ localStore = {};
16
+ }),
17
+ get length() {
18
+ return Object.keys(localStore).length;
19
+ },
20
+ key: jest.fn((i) => Object.keys(localStore)[i] || null)
21
+ };
22
+
23
+ // Override localStorage on window (jsdom provides window)
24
+ Object.defineProperty(window, 'localStorage', {
25
+ value: localStorageMock,
26
+ writable: true
27
+ });
28
+
29
+ // Also set on global for Node environment
30
+ Object.defineProperty(global, 'localStorage', {
31
+ value: localStorageMock,
32
+ writable: true
33
+ });
34
+
35
+ // Mock window.history.pushState and replaceState
36
+ window.history.pushState = jest.fn();
37
+ window.history.replaceState = jest.fn();
38
+
39
+ // Reset mocks and store before each test
40
+ beforeEach(() => {
41
+ localStore = {};
42
+ localStorageMock.getItem.mockClear();
43
+ localStorageMock.setItem.mockClear();
44
+ localStorageMock.removeItem.mockClear();
45
+ localStorageMock.clear.mockClear();
46
+
47
+ // Reset window.location.hash using the proper jsdom approach
48
+ window.location.hash = '';
49
+
50
+ jest.clearAllMocks();
51
+ });
package/src/utils.js CHANGED
@@ -1,64 +1,64 @@
1
- const storageMap = {};
2
- export const storage = {
3
- set: (key, value) => {
4
- return isBrowser() ? localStorage.setItem(key, value) : (storageMap[key] = value);
5
- },
6
- get: (key) => {
7
- return isBrowser() ? localStorage.getItem(key) : storageMap[key];
8
- },
9
- remove: (key) => {
10
- return isBrowser() ? localStorage.removeItem(key) : delete storageMap[key];
11
- }
12
- };
13
- export const isBrowser = () => {
14
- return !(typeof process === 'object' && String(process) === '[object process]');
15
- }
16
-
17
- /**
18
- * Detect if an error is likely a CORS error.
19
- * CORS errors in browsers are intentionally vague for security, so we look for common patterns.
20
- */
21
- export function isCorsError(error) {
22
- if (!error) return false;
23
- const msg = (error.message || error.toString() || '').toLowerCase();
24
-
25
- // Common CORS error patterns
26
- return (
27
- msg.includes('cors') ||
28
- msg.includes('cross-origin') ||
29
- msg.includes('network error') ||
30
- msg.includes('failed to fetch') ||
31
- msg.includes('networkerror') ||
32
- msg.includes('load failed') ||
33
- // Socket.io specific patterns for CORS failures
34
- msg.includes('xhr poll error') ||
35
- msg.includes('websocket error') ||
36
- (msg.includes('transport') && msg.includes('error'))
37
- );
38
- }
39
-
40
- /**
41
- * Log helpful CORS debugging guidance to the console.
42
- */
43
- export function logCorsHelp(context, url) {
44
- console.error(`
45
- ╔══════════════════════════════════════════════════════════════════════════════╗
46
- ║ GameGlue SDK: Possible CORS Error Detected ║
47
- ╠══════════════════════════════════════════════════════════════════════════════╣
48
- ║ Context: ${context.padEnd(66)}║
49
- ║ URL: ${(url || 'unknown').substring(0, 70).padEnd(70)}║
50
- ╠══════════════════════════════════════════════════════════════════════════════╣
51
- ║ This error typically means the server rejected the request due to CORS. ║
52
- ║ ║
53
- ║ To fix this, add your origin to your app's allowed Web Origins: ║
54
- ║ ║
55
- ║ 1. Go to the GameGlue Developer Portal (https://developer.gameglue.gg) ║
56
- ║ 2. Select your app ║
57
- ║ 3. Add your origin to "Web Origins" and save ║
58
- ║ ║
59
- ║ IMPORTANT: Protocol and port must match exactly. ║
60
- ║ http://localhost:3000 and https://localhost:3000 are different origins. ║
61
- ║ http://localhost:3000 and http://localhost:5000 are different origins. ║
62
- ╚══════════════════════════════════════════════════════════════════════════════╝
63
- `);
1
+ const storageMap = {};
2
+ export const storage = {
3
+ set: (key, value) => {
4
+ return isBrowser() ? localStorage.setItem(key, value) : (storageMap[key] = value);
5
+ },
6
+ get: (key) => {
7
+ return isBrowser() ? localStorage.getItem(key) : storageMap[key];
8
+ },
9
+ remove: (key) => {
10
+ return isBrowser() ? localStorage.removeItem(key) : delete storageMap[key];
11
+ }
12
+ };
13
+ export const isBrowser = () => {
14
+ return !(typeof process === 'object' && String(process) === '[object process]');
15
+ }
16
+
17
+ /**
18
+ * Detect if an error is likely a CORS error.
19
+ * CORS errors in browsers are intentionally vague for security, so we look for common patterns.
20
+ */
21
+ export function isCorsError(error) {
22
+ if (!error) return false;
23
+ const msg = (error.message || error.toString() || '').toLowerCase();
24
+
25
+ // Common CORS error patterns
26
+ return (
27
+ msg.includes('cors') ||
28
+ msg.includes('cross-origin') ||
29
+ msg.includes('network error') ||
30
+ msg.includes('failed to fetch') ||
31
+ msg.includes('networkerror') ||
32
+ msg.includes('load failed') ||
33
+ // Socket.io specific patterns for CORS failures
34
+ msg.includes('xhr poll error') ||
35
+ msg.includes('websocket error') ||
36
+ (msg.includes('transport') && msg.includes('error'))
37
+ );
38
+ }
39
+
40
+ /**
41
+ * Log helpful CORS debugging guidance to the console.
42
+ */
43
+ export function logCorsHelp(context, url) {
44
+ console.error(`
45
+ ╔══════════════════════════════════════════════════════════════════════════════╗
46
+ ║ GameGlue SDK: Possible CORS Error Detected ║
47
+ ╠══════════════════════════════════════════════════════════════════════════════╣
48
+ ║ Context: ${context.padEnd(66)}║
49
+ ║ URL: ${(url || 'unknown').substring(0, 70).padEnd(70)}║
50
+ ╠══════════════════════════════════════════════════════════════════════════════╣
51
+ ║ This error typically means the server rejected the request due to CORS. ║
52
+ ║ ║
53
+ ║ To fix this, add your origin to your app's allowed Web Origins: ║
54
+ ║ ║
55
+ ║ 1. Go to the GameGlue Developer Portal (https://developer.gameglue.gg) ║
56
+ ║ 2. Select your app ║
57
+ ║ 3. Add your origin to "Web Origins" and save ║
58
+ ║ ║
59
+ ║ IMPORTANT: Protocol and port must match exactly. ║
60
+ ║ http://localhost:3000 and https://localhost:3000 are different origins. ║
61
+ ║ http://localhost:3000 and http://localhost:5000 are different origins. ║
62
+ ╚══════════════════════════════════════════════════════════════════════════════╝
63
+ `);
64
64
  }