Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ examples/**/*
packages/internal/generated-clients/src/

# put module specific ignore paths here
packages/game-bridge/scripts/**/*.js
28 changes: 28 additions & 0 deletions packages/auth/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,14 @@ export function isAPIError(error: any): error is imx.APIError {

type AxiosLikeError = {
response?: {
status?: number;
data?: unknown;
};
config?: {
url?: string;
baseURL?: string;
method?: string;
};
};

const extractApiError = (error: unknown): imx.APIError | undefined => {
Expand All @@ -59,6 +65,23 @@ const extractApiError = (error: unknown): imx.APIError | undefined => {
return undefined;
};

const appendHttpDebugInfo = (message: string, error: unknown): string => {
const e = error as AxiosLikeError;
const status = e?.response?.status;
const url = e?.config?.url;
const baseURL = e?.config?.baseURL;
const fullUrl = (
typeof url === 'string' && typeof baseURL === 'string' && !/^https?:\/\//i.test(url)
? `${baseURL}${url}`
: url
);

if (status == null && fullUrl == null) return message;
if (message.includes('[httpStatus=')) return message;

return `${message} [httpStatus=${status ?? 'unknown'} url=${fullUrl ?? 'unknown'}]`;
};

export class PassportError extends Error {
public type: PassportErrorType;

Expand Down Expand Up @@ -88,6 +111,11 @@ export const withPassportError = async <T>(
errorMessage = (error as Error).message;
}

// Debug aid: preserve which HTTP endpoint/status failed for IMX offchain registration.
if (customErrorType === PassportErrorType.USER_REGISTRATION_ERROR) {
errorMessage = appendHttpDebugInfo(errorMessage, error);
}

throw new PassportError(errorMessage, customErrorType);
}
};
18 changes: 13 additions & 5 deletions packages/game-bridge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"parcel": "^2.13.3"
},
"scripts": {
"build": "parcel build --no-cache --no-scope-hoist",
"build:local": "parcel build --no-cache --no-scope-hoist && pnpm updateSdkVersion",
"build": "parcel build --no-cache --no-scope-hoist && node scripts/fixUnityBuild.js",
"build:local": "parcel build --no-cache --no-scope-hoist && node scripts/fixUnityBuild.js && pnpm updateSdkVersion",
"lint": "eslint ./src --ext .ts,.jsx,.tsx --max-warnings=0",
"start": "parcel",
"updateSdkVersion": "./scripts/updateSdkVersion.sh"
Expand All @@ -25,17 +25,25 @@
"unity": {
"context": "browser",
"source": "src/index.html",
"outputFormat": "global",
"scopeHoist": false,
"isLibrary": false,
"engines": {
"browsers": "Chrome 90"
"browsers": "Chrome 137"
}
},
"unreal": {
"outputFormat": "global",
"context": "browser",
"source": "src/index.ts",
"scopeHoist": false,
"isLibrary": false,
"engines": {
"browsers": "Chrome 90"
"browsers": "Chrome 137"
}
}
}
},
"browserslist": [
"Chrome 137"
]
}
77 changes: 77 additions & 0 deletions packages/game-bridge/scripts/fixUnityBuild.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env node

const fs = require('fs');
const path = require('path');

const DIST_DIR = path.join(__dirname, '..', 'dist', 'unity');
const HTML_FILE = path.join(DIST_DIR, 'index.html');

console.log('🔧 Fixing Unity build...');
console.log(`📁 Dist directory: ${DIST_DIR}`);

if (!fs.existsSync(DIST_DIR)) {
console.error('❌ Dist directory not found!');
process.exit(1);
}

// Find all JS files (excluding .map files)
const jsFiles = fs.readdirSync(DIST_DIR)
.filter(f => f.endsWith('.js') && !f.endsWith('.map'))
.sort((a, b) => {
// Main bundle first
if (a.startsWith('game-bridge')) return -1;
if (b.startsWith('game-bridge')) return 1;
return a.localeCompare(b);
});

console.log(`📦 Found ${jsFiles.length} JS file(s):`, jsFiles);

if (jsFiles.length === 0) {
console.error('❌ No JS files found to inline!');
process.exit(1);
}

// Combine all JS files
let combinedJs = '';
for (const jsFile of jsFiles) {
const jsPath = path.join(DIST_DIR, jsFile);
const jsContent = fs.readFileSync(jsPath, 'utf8');
combinedJs += jsContent + '\n';
console.log(` ✅ ${jsFile}: ${jsContent.length} bytes`);
}

console.log(`📊 Total combined JavaScript: ${combinedJs.length} bytes`);

// Create new HTML with inlined JavaScript
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>GameSDK Bridge</title>
<script>${combinedJs}</script>
</head>
<body>
</body>
</html>`;

// Write the new HTML file
fs.writeFileSync(HTML_FILE, html, 'utf8');
console.log(`✅ Unity build fixed successfully!`);
console.log(`📄 Output: ${HTML_FILE} (${html.length} bytes)`);

// Clean up: remove JS files and source maps
console.log('🧹 Cleaning up external JS files...');
for (const jsFile of jsFiles) {
const jsPath = path.join(DIST_DIR, jsFile);
fs.unlinkSync(jsPath);
console.log(` 🗑️ Removed ${jsFile}`);

const mapPath = jsPath + '.map';
if (fs.existsSync(mapPath)) {
fs.unlinkSync(mapPath);
console.log(` 🗑️ Removed ${jsFile}.map`);
}
}

console.log('✨ Done!');

5 changes: 1 addition & 4 deletions packages/game-bridge/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@
<head>
<meta charset="utf-8">
<title>GameSDK Bridge</title>
<script type="module">
import "./index.ts";
</script>
<script type="module" src="./index.ts"></script>
</head>

<body>
<h1>Bridge Running</h1>
</body>

</html>
147 changes: 124 additions & 23 deletions packages/game-bridge/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,57 @@ const PASSPORT_FUNCTIONS = {
},
};

function getHttpErrorSummary(err: unknown): {
status?: number;
fullUrl?: string;
traceId?: string;
requestId?: string;
cfRay?: string;
} {
const e: any = err as any;
const status: number | undefined = e?.response?.status;
const url: string | undefined = e?.config?.url;
const baseURL: string | undefined = e?.config?.baseURL;
let fullUrl = typeof url === 'string' && typeof baseURL === 'string' && !/^https?:\/\//i.test(url)
? `${baseURL}${url}`
: url;

// Remove query parameters to avoid exposing sensitive data (tokens, API keys, etc.)
if (typeof fullUrl === 'string') {
const queryIndex = fullUrl.indexOf('?');
if (queryIndex !== -1) {
fullUrl = fullUrl.substring(0, queryIndex);
}
}

const headers: Record<string, string> | undefined = e?.response?.headers;
const traceId = headers?.['x-amzn-trace-id'] ?? headers?.['x-trace-id'];
const requestId = headers?.['x-amzn-requestid']
?? headers?.['x-amzn-request-id']
?? headers?.['x-request-id'];
const cfRay = headers?.['cf-ray'];

return {
status,
fullUrl,
traceId,
requestId,
cfRay,
};
}

function parseHttpStatusSuffix(message: string): { status?: number; url?: string } {
// Matches our existing suffix formats like:
// "... [httpStatus=500 url=https://...]" or
// "... [httpStatus=500 url=https://... trace=... ...]"
const m = message.match(/\[httpStatus=([^\s\]]+)\s+url=([^\s\]]+)[^\]]*\]/);
if (!m) return {};
const statusStr = m[1];
const url = m[2];
const status = statusStr && /^[0-9]+$/.test(statusStr) ? Number(statusStr) : undefined;
return { status, url };
}

// To notify game engine that this file is loaded
const initRequest = 'init';
const initRequestId = '1';
Expand Down Expand Up @@ -126,27 +177,31 @@ type VersionInfo = {

const callbackToGame = (data: CallbackData) => {
const message = JSON.stringify(data);
console.log(`callbackToGame: ${message}`);
if (typeof window.ue !== 'undefined') {
if (typeof window.ue.jsconnector === 'undefined') {
const unrealError = 'Unreal JSConnector not defined';
console.error(unrealError);
throw new Error(unrealError);
} else {

try {
if (typeof window.ue !== 'undefined') {
if (typeof window.ue.jsconnector === 'undefined') {
const unrealError = 'Unreal JSConnector not defined';
console.error('[GAME-BRIDGE]', unrealError);
throw new Error(unrealError);
}
window.ue.jsconnector.sendtogame(message);
} else if (typeof blu_event !== 'undefined') {
blu_event('sendtogame', message);
} else if (typeof UnityPostMessage !== 'undefined') {
UnityPostMessage(message);
} else if (typeof window.Unity !== 'undefined') {
window.Unity.call(message);
} else if (typeof window.uwb !== 'undefined') {
window.uwb.ExecuteJsMethod('callback', message);
} else {
const gameBridgeError = 'No available game callbacks to call from ImmutableSDK game-bridge';
console.error('[GAME-BRIDGE]', gameBridgeError);
throw new Error(gameBridgeError);
}
} else if (typeof blu_event !== 'undefined') {
blu_event('sendtogame', message);
} else if (typeof UnityPostMessage !== 'undefined') {
UnityPostMessage(message);
} else if (typeof window.Unity !== 'undefined') {
window.Unity.call(message);
} else if (typeof window.uwb !== 'undefined') {
window.uwb.ExecuteJsMethod('callback', message);
} else {
const gameBridgeError = 'No available game callbacks to call from ImmutableSDK game-bridge';
console.error(gameBridgeError);
throw new Error(gameBridgeError);
} catch (error) {
console.error('[GAME-BRIDGE] Error in callbackToGame:', error);
throw error;
}
};

Expand Down Expand Up @@ -224,9 +279,9 @@ window.callFunction = async (jsonData: string) => {
const request = JSON.parse(data);
const redirect: string | null = request?.redirectUri;
const logoutMode: 'silent' | 'redirect' = request?.isSilentLogout === true ? 'silent' : 'redirect';

if (!passportClient || passportInitData !== data) {
passportInitData = data;
console.log(`Connecting to ${request.environment} environment`);

let passportConfig: passport.PassportModuleConfiguration;

Expand Down Expand Up @@ -259,6 +314,8 @@ window.callFunction = async (jsonData: string) => {
chainID: 5,
coreContractAddress: '0xd05323731807A35599BF9798a1DE15e89d6D6eF1',
registrationContractAddress: '0x7EB840223a3b1E0e8D54bF8A6cd83df5AFfC88B2',
sdkVersion: sdkVersionTag,
baseConfig,
}),
},
}),
Expand All @@ -283,9 +340,15 @@ window.callFunction = async (jsonData: string) => {
};
}

passportClient = new passport.Passport(passportConfig);
trackDuration(moduleName, 'initialisedPassport', mt(markStart));
try {
passportClient = new passport.Passport(passportConfig);
trackDuration(moduleName, 'initialisedPassport', mt(markStart));
} catch (initError) {
console.error('[GAME-BRIDGE] Error creating Passport client:', initError);
throw initError;
}
}

callbackToGame({
responseFor: fxName,
requestId,
Expand Down Expand Up @@ -805,6 +868,41 @@ window.callFunction = async (jsonData: string) => {
wrappedError = error;
}

// Make endpoint visible in Unity Output for debugging (CI logs don't include JS console).
const {
status,
fullUrl,
traceId,
requestId: httpRequestId,
cfRay,
} = getHttpErrorSummary(error);
if (
fxName === PASSPORT_FUNCTIONS.imx.registerOffchain
&& wrappedError instanceof Error
) {
// Some upstream errors embed "[httpStatus=... url=...]" only in the message string
// without preserving axios-like fields. Parse what we can so we can still enrich.
const parsed = parseHttpStatusSuffix(wrappedError.message);
const effectiveStatus = status ?? parsed.status;
const effectiveUrl = fullUrl ?? parsed.url;
const suffix = ` [httpStatus=${effectiveStatus ?? 'unknown'}`
+ ` url=${effectiveUrl ?? 'unknown'}`
+ ` trace=${traceId ?? 'unknown'}`
+ ` reqId=${httpRequestId ?? 'unknown'}`
+ ` cfRay=${cfRay ?? 'unknown'}]`;
// If a previous layer already added a minimal suffix like:
// "... [httpStatus=500 url=...]"
// upgrade it to include trace/reqId/resp instead of skipping.
if (wrappedError.message.includes('[httpStatus=')) {
if (!wrappedError.message.includes('trace=')) {
const upgraded = wrappedError.message.replace(/\[httpStatus=[^\]]*\]/g, suffix.trim());
wrappedError = new Error(upgraded);
}
} else {
wrappedError = new Error(`${wrappedError.message}${suffix}`);
}
}

const errorType = error instanceof passport.PassportError
? error?.type
: undefined;
Expand All @@ -827,7 +925,10 @@ window.callFunction = async (jsonData: string) => {
responseFor: fxName,
requestId,
success: false,
error: error?.message !== null && error?.message !== undefined ? error.message : 'Error',
// IMPORTANT: return the wrapped error message so we include extra HTTP diagnostics
// (httpStatus/url/trace/reqId/resp) in Unity CI logs.
error: wrappedError?.message
?? (error?.message !== null && error?.message !== undefined ? error.message : 'Error'),
errorType: error instanceof passport.PassportError ? error?.type : null,
});
}
Expand Down
Loading