brainclouds2s
Advanced tools
| 'use strict'; | ||
| //---------------------------------------------------- | ||
| // brainCloud client source code | ||
| // Copyright 2026 bitHeads, inc. | ||
| //---------------------------------------------------- | ||
| var https = require('https'); | ||
| var brainclouds2s = require('./brainclouds2s'); | ||
| /** | ||
| * S2S service for brainCloud Global File V3 operations. | ||
| * | ||
| * Usage: | ||
| * const S2S = require('./brainclouds2s'); | ||
| * const GFV3 = require('./brainclouds2s-globalfilev3'); | ||
| * | ||
| * let context = S2S.init(appId, serverName, serverSecret, s2sUrl, false); | ||
| * S2S.authenticate(context, (ctx, result) => { | ||
| * GFV3.sysGetGlobalFileList(ctx, "", true, (ctx, result) => { ... }); | ||
| * }); | ||
| * | ||
| * File upload is a two-step process: | ||
| * 1. SYS_PREPARE_UPLOAD is sent via the S2S dispatcher and returns an uploadId + uploadUrl. | ||
| * 2. The file bytes are POSTed as multipart/form-data to the upload endpoint. | ||
| */ | ||
| // Default upload URL (fallback when context.url does not resolve to the upload endpoint) | ||
| var DEFAULT_UPLOAD_PATH = '/s2suploader/globalfile/upload'; | ||
| // ----------------------------------------------------------------------- | ||
| // File Info / Query | ||
| // ----------------------------------------------------------------------- | ||
| /** | ||
| * Returns metadata for a global file identified by its fileId. | ||
| * @param {object} context - S2S context returned by brainclouds2s.init() | ||
| * @param {string} fileId - Unique file identifier | ||
| * @param {function} callback - (context, result) where result is the parsed response | ||
| */ | ||
| exports.sysGetFileInfo = (context, fileId, callback) => { | ||
| brainclouds2s.request(context, { | ||
| service: 'globalFileV3', | ||
| operation: 'SYS_GET_FILE_INFO', | ||
| data: { fileId: fileId } | ||
| }, callback); | ||
| }; | ||
| /** | ||
| * Returns metadata for a global file identified by folder path and filename. | ||
| * @param {object} context - S2S context returned by brainclouds2s.init() | ||
| * @param {string} folderPath - Folder path (e.g. "myFolder/subFolder") | ||
| * @param {string} filename - File name | ||
| * @param {function} callback - (context, result) where result is the parsed response | ||
| */ | ||
| exports.sysGetFileInfoSimple = (context, folderPath, filename, callback) => { | ||
| brainclouds2s.request(context, { | ||
| service: 'globalFileV3', | ||
| operation: 'SYS_GET_FILE_INFO_SIMPLE', | ||
| data: { folderPath: folderPath, filename: filename } | ||
| }, callback); | ||
| }; | ||
| /** | ||
| * Returns true if a file with the given name exists in the specified folder. | ||
| * @param {object} context - S2S context returned by brainclouds2s.init() | ||
| * @param {string} folderPath - Folder path | ||
| * @param {string} filename - File name | ||
| * @param {function} callback - (context, result) where result is the parsed response | ||
| */ | ||
| exports.sysCheckFilenameExists = (context, folderPath, filename, callback) => { | ||
| brainclouds2s.request(context, { | ||
| service: 'globalFileV3', | ||
| operation: 'SYS_CHECK_FILENAME_EXISTS', | ||
| data: { folderPath: folderPath, filename: filename } | ||
| }, callback); | ||
| }; | ||
| /** | ||
| * Returns true if a file exists at the given full path (e.g. "/folder/sub/file.txt"). | ||
| * @param {object} context - S2S context returned by brainclouds2s.init() | ||
| * @param {string} fullpathFilename - Full path including filename | ||
| * @param {function} callback - (context, result) where result is the parsed response | ||
| */ | ||
| exports.sysCheckFullpathFilenameExists = (context, fullpathFilename, callback) => { | ||
| brainclouds2s.request(context, { | ||
| service: 'globalFileV3', | ||
| operation: 'SYS_CHECK_FULLPATH_FILENAME_EXISTS', | ||
| data: { fullPathFilename: fullpathFilename } | ||
| }, callback); | ||
| }; | ||
| /** | ||
| * Returns the CDN URL for the global file identified by fileId. | ||
| * @param {object} context - S2S context returned by brainclouds2s.init() | ||
| * @param {string} fileId - Unique file identifier | ||
| * @param {function} callback - (context, result) where result is the parsed response | ||
| */ | ||
| exports.sysGetGlobalCDNUrl = (context, fileId, callback) => { | ||
| brainclouds2s.request(context, { | ||
| service: 'globalFileV3', | ||
| operation: 'SYS_GET_GLOBAL_CDN_URL', | ||
| data: { fileId: fileId } | ||
| }, callback); | ||
| }; | ||
| /** | ||
| * Lists all global files under the given folder path. | ||
| * Pass folderPath="" and recurse=true to list the entire tree. | ||
| * @param {object} context - S2S context returned by brainclouds2s.init() | ||
| * @param {string} folderPath - Folder path; use "" for root | ||
| * @param {boolean} recurse - If true, list files in sub-folders as well | ||
| * @param {function} callback - (context, result) where result is the parsed response | ||
| */ | ||
| exports.sysGetGlobalFileList = (context, folderPath, recurse, callback) => { | ||
| brainclouds2s.request(context, { | ||
| service: 'globalFileV3', | ||
| operation: 'SYS_GET_GLOBAL_FILE_LIST', | ||
| data: { folderPath: folderPath, recurse: recurse } | ||
| }, callback); | ||
| }; | ||
| // ----------------------------------------------------------------------- | ||
| // File Management | ||
| // ----------------------------------------------------------------------- | ||
| /** | ||
| * Moves a file from a user's personal cloud storage into the global file system. | ||
| * @param {object} context - S2S context returned by brainclouds2s.init() | ||
| * @param {string} userProfileId - User profile ID | ||
| * @param {string} userCloudPath - Path in the user's cloud | ||
| * @param {string} userCloudFilename - Filename in the user's cloud | ||
| * @param {string} globalTreeId - Target folder tree ID | ||
| * @param {string} globalFilename - Filename in the global file system | ||
| * @param {boolean} overwriteIfPresent - Overwrite existing file if true | ||
| * @param {function} callback - (context, result) where result is the parsed response | ||
| */ | ||
| exports.sysMoveToGlobalFile = (context, userProfileId, userCloudPath, userCloudFilename, | ||
| globalTreeId, globalFilename, overwriteIfPresent, callback) => { | ||
| brainclouds2s.request(context, { | ||
| service: 'globalFileV3', | ||
| operation: 'SYS_MOVE_TO_GLOBAL_FILE', | ||
| data: { | ||
| userProfileId: userProfileId, | ||
| userCloudPath: userCloudPath, | ||
| userCloudFilename: userCloudFilename, | ||
| globalTreeId: globalTreeId, | ||
| globalFilename: globalFilename, | ||
| overwriteIfPresent: overwriteIfPresent | ||
| } | ||
| }, callback); | ||
| }; | ||
| /** | ||
| * Copies a global file to another folder, optionally with a new name. | ||
| * @param {object} context - S2S context returned by brainclouds2s.init() | ||
| * @param {string} fileId - File to copy | ||
| * @param {number} version - File version; pass -1 for latest | ||
| * @param {string} newTreeId - Destination folder tree ID | ||
| * @param {number} treeVersion - Destination tree version; pass -1 to skip check | ||
| * @param {string} newFilename - Filename in the destination folder | ||
| * @param {boolean} overwriteIfPresent - Overwrite existing file if true | ||
| * @param {function} callback - (context, result) where result is the parsed response | ||
| */ | ||
| exports.sysCopyGlobalFile = (context, fileId, version, newTreeId, treeVersion, | ||
| newFilename, overwriteIfPresent, callback) => { | ||
| brainclouds2s.request(context, { | ||
| service: 'globalFileV3', | ||
| operation: 'SYS_COPY_GLOBAL_FILE', | ||
| data: { | ||
| fileId: fileId, | ||
| version: version, | ||
| newTreeId: newTreeId, | ||
| treeVersion: treeVersion, | ||
| newFilename: newFilename, | ||
| overwriteIfPresent: overwriteIfPresent | ||
| } | ||
| }, callback); | ||
| }; | ||
| /** | ||
| * Moves a global file to another folder, optionally with a new name. | ||
| * @param {object} context - S2S context returned by brainclouds2s.init() | ||
| * @param {string} fileId - File to move | ||
| * @param {number} version - File version; pass -1 for latest | ||
| * @param {string} newTreeId - Destination folder tree ID | ||
| * @param {number} treeVersion - Destination tree version; pass -1 to skip check | ||
| * @param {string} newFilename - Filename in the destination folder | ||
| * @param {boolean} overwriteIfPresent - Overwrite existing file if true | ||
| * @param {function} callback - (context, result) where result is the parsed response | ||
| */ | ||
| exports.sysMoveGlobalFile = (context, fileId, version, newTreeId, treeVersion, | ||
| newFilename, overwriteIfPresent, callback) => { | ||
| brainclouds2s.request(context, { | ||
| service: 'globalFileV3', | ||
| operation: 'SYS_MOVE_GLOBAL_FILE', | ||
| data: { | ||
| fileId: fileId, | ||
| version: version, | ||
| newTreeId: newTreeId, | ||
| treeVersion: treeVersion, | ||
| newFilename: newFilename, | ||
| overwriteIfPresent: overwriteIfPresent | ||
| } | ||
| }, callback); | ||
| }; | ||
| /** | ||
| * Deletes a single global file. Pass version=-1 to delete without a version check. | ||
| * @param {object} context - S2S context returned by brainclouds2s.init() | ||
| * @param {string} fileId - File to delete | ||
| * @param {number} version - File version; pass -1 to skip check | ||
| * @param {string} filename - Filename | ||
| * @param {function} callback - (context, result) where result is the parsed response | ||
| */ | ||
| exports.sysDeleteGlobalFile = (context, fileId, version, filename, callback) => { | ||
| brainclouds2s.request(context, { | ||
| service: 'globalFileV3', | ||
| operation: 'SYS_DELETE_GLOBAL_FILE', | ||
| data: { fileId: fileId, version: version, filename: filename } | ||
| }, callback); | ||
| }; | ||
| /** | ||
| * Deletes all global files in the specified folder. | ||
| * Set recurse=true to also delete files in sub-folders. | ||
| * @param {object} context - S2S context returned by brainclouds2s.init() | ||
| * @param {string} treeId - Folder tree ID | ||
| * @param {string} folderPath - Folder path | ||
| * @param {number} treeVersion - Tree version; pass -1 to skip check | ||
| * @param {boolean} recurse - Delete files in sub-folders as well | ||
| * @param {function} callback - (context, result) where result is the parsed response | ||
| */ | ||
| exports.sysDeleteGlobalFiles = (context, treeId, folderPath, treeVersion, recurse, callback) => { | ||
| brainclouds2s.request(context, { | ||
| service: 'globalFileV3', | ||
| operation: 'SYS_DELETE_GLOBAL_FILES', | ||
| data: { treeId: treeId, folderPath: folderPath, treeVersion: treeVersion, recurse: recurse } | ||
| }, callback); | ||
| }; | ||
| // ----------------------------------------------------------------------- | ||
| // Folder Management | ||
| // ----------------------------------------------------------------------- | ||
| /** | ||
| * Creates a new folder at the given path. | ||
| * Set createInterimDirectories=true to auto-create any missing parent folders. | ||
| * @param {object} context - S2S context returned by brainclouds2s.init() | ||
| * @param {string} folderPath - Path for the new folder | ||
| * @param {number} treeVersion - Tree version; pass -1 to skip check | ||
| * @param {string} name - Folder name | ||
| * @param {string} desc - Folder description | ||
| * @param {boolean} createInterimDirectories - Create missing parent folders if true | ||
| * @param {function} callback - (context, result) where result is the parsed response | ||
| */ | ||
| exports.sysCreateFolder = (context, folderPath, treeVersion, name, desc, | ||
| createInterimDirectories, callback) => { | ||
| brainclouds2s.request(context, { | ||
| service: 'globalFileV3', | ||
| operation: 'SYS_CREATE_FOLDER', | ||
| data: { | ||
| folderPath: folderPath, | ||
| treeVersion: treeVersion, | ||
| name: name, | ||
| desc: desc, | ||
| createInterimDirectories: createInterimDirectories | ||
| } | ||
| }, callback); | ||
| }; | ||
| /** | ||
| * Moves a folder to a new path, optionally renaming it. | ||
| * @param {object} context - S2S context returned by brainclouds2s.init() | ||
| * @param {string} treeId - Folder tree ID | ||
| * @param {number} treeVersion - Tree version; pass -1 to skip check | ||
| * @param {string} newFolderPath - Destination path | ||
| * @param {string} updatedName - New folder name | ||
| * @param {boolean} createInterimDirectories - Create missing parent folders if true | ||
| * @param {function} callback - (context, result) where result is the parsed response | ||
| */ | ||
| exports.sysMoveFolder = (context, treeId, treeVersion, newFolderPath, updatedName, | ||
| createInterimDirectories, callback) => { | ||
| brainclouds2s.request(context, { | ||
| service: 'globalFileV3', | ||
| operation: 'SYS_MOVE_FOLDER', | ||
| data: { | ||
| treeId: treeId, | ||
| treeVersion: treeVersion, | ||
| newFolderPath: newFolderPath, | ||
| updatedName: updatedName, | ||
| createInterimDirectories: createInterimDirectories | ||
| } | ||
| }, callback); | ||
| }; | ||
| /** | ||
| * Renames a folder in place. | ||
| * @param {object} context - S2S context returned by brainclouds2s.init() | ||
| * @param {string} treeId - Folder tree ID | ||
| * @param {number} treeVersion - Tree version; pass -1 to skip check | ||
| * @param {string} updatedName - New folder name | ||
| * @param {function} callback - (context, result) where result is the parsed response | ||
| */ | ||
| exports.sysRenameFolder = (context, treeId, treeVersion, updatedName, callback) => { | ||
| brainclouds2s.request(context, { | ||
| service: 'globalFileV3', | ||
| operation: 'SYS_RENAME_FOLDER', | ||
| data: { treeId: treeId, treeVersion: treeVersion, updatedName: updatedName } | ||
| }, callback); | ||
| }; | ||
| /** | ||
| * Resolves the treeId for a folder given its full path (e.g. "/folder/sub/"). | ||
| * @param {object} context - S2S context returned by brainclouds2s.init() | ||
| * @param {string} fullFolderPath - Full folder path | ||
| * @param {function} callback - (context, result) where result is the parsed response | ||
| */ | ||
| exports.sysLookupFolder = (context, fullFolderPath, callback) => { | ||
| brainclouds2s.request(context, { | ||
| service: 'globalFileV3', | ||
| operation: 'SYS_LOOKUP_FOLDER', | ||
| data: { fullFolderPath: fullFolderPath } | ||
| }, callback); | ||
| }; | ||
| /** | ||
| * Deletes a folder. Set force=true to also delete any files and sub-folders inside it. | ||
| * @param {object} context - S2S context returned by brainclouds2s.init() | ||
| * @param {string} treeId - Folder tree ID | ||
| * @param {string} folderPath - Folder path | ||
| * @param {number} treeVersion - Tree version; pass -1 to skip check | ||
| * @param {boolean} force - Delete files and sub-folders inside the folder if true | ||
| * @param {function} callback - (context, result) where result is the parsed response | ||
| */ | ||
| exports.sysDeleteFolder = (context, treeId, folderPath, treeVersion, force, callback) => { | ||
| brainclouds2s.request(context, { | ||
| service: 'globalFileV3', | ||
| operation: 'SYS_DELETE_FOLDER', | ||
| data: { treeId: treeId, folderPath: folderPath, treeVersion: treeVersion, force: force } | ||
| }, callback); | ||
| }; | ||
| // ----------------------------------------------------------------------- | ||
| // Upload | ||
| // ----------------------------------------------------------------------- | ||
| /** | ||
| * Uploads a file to the brainCloud Global File V3 system via S2S. | ||
| * | ||
| * Internally performs SYS_PREPARE_UPLOAD to obtain an uploadId, then POSTs the | ||
| * file bytes to the upload endpoint as multipart/form-data. Metadata (gameId, | ||
| * uploadId) travels as URL query parameters; only the file bytes are in the body. | ||
| * | ||
| * @param {object} context - S2S context returned by brainclouds2s.init() | ||
| * @param {string} treeId - Folder tree ID (use "_root_" for root; call sysLookupFolder for sub-folders) | ||
| * @param {string} filename - Name of the file as it will appear in brainCloud | ||
| * @param {boolean} overwriteIfPresent - Replace any existing file with the same name | ||
| * @param {Buffer} fileData - File content as a Node.js Buffer (or any Uint8Array) | ||
| * @param {function} callback - (context, result) where result is the parsed upload response | ||
| */ | ||
| exports.uploadGlobalFile = (context, treeId, filename, overwriteIfPresent, fileData, callback) => { | ||
| if (context.logEnabled) { | ||
| console.log('[GlobalFileV3] Preparing upload: ' + filename + | ||
| ' (' + fileData.length + ' bytes) treeId=' + treeId); | ||
| } | ||
| brainclouds2s.request(context, { | ||
| service: 'globalFileV3', | ||
| operation: 'SYS_PREPARE_UPLOAD', | ||
| data: { | ||
| treeId: treeId, | ||
| filename: filename, | ||
| overwriteIfPresent: overwriteIfPresent, | ||
| fileSize: fileData.length | ||
| } | ||
| }, (ctx, result) => { | ||
| if (!result || result.status !== 200) { | ||
| if (context.logEnabled) { | ||
| console.log('[GlobalFileV3] SYS_PREPARE_UPLOAD failed: ' + JSON.stringify(result)); | ||
| } | ||
| if (callback) callback(ctx, result); | ||
| return; | ||
| } | ||
| var fileDetails = result.data && result.data.fileDetails; | ||
| if (!fileDetails || !fileDetails.uploadId) { | ||
| if (context.logEnabled) { | ||
| console.log('[GlobalFileV3] SYS_PREPARE_UPLOAD missing fileDetails/uploadId: ' + | ||
| JSON.stringify(result)); | ||
| } | ||
| if (callback) callback(ctx, result); | ||
| return; | ||
| } | ||
| var uploadId = fileDetails.uploadId; | ||
| var uploadUrl = buildUploadUrl(context, fileDetails, uploadId); | ||
| if (context.logEnabled) { | ||
| console.log('[GlobalFileV3] Uploading to: ' + uploadUrl); | ||
| } | ||
| sendFileUpload(context, uploadUrl, filename, fileData, callback); | ||
| }); | ||
| }; | ||
| // ----------------------------------------------------------------------- | ||
| // Internal helpers | ||
| // ----------------------------------------------------------------------- | ||
| /** | ||
| * Constructs an absolute upload URL from the prepare response. | ||
| * If the server returned a relative uploadUrl we prefix it with the scheme and host. | ||
| * Falls back to deriving the URL from context.url. | ||
| */ | ||
| function buildUploadUrl(context, fileDetails, uploadId) { | ||
| if (fileDetails.uploadUrl) { | ||
| var relativeUrl = fileDetails.uploadUrl; | ||
| if (relativeUrl.startsWith('http')) { | ||
| return relativeUrl; | ||
| } | ||
| // Relative path returned by server — prefix with scheme + host | ||
| return 'https://' + context.url + relativeUrl; | ||
| } | ||
| // Fallback: construct from context.url (hostname only) | ||
| return 'https://' + context.url + DEFAULT_UPLOAD_PATH + | ||
| '?gameId=' + encodeURIComponent(context.appId) + | ||
| '&uploadId=' + encodeURIComponent(uploadId); | ||
| } | ||
| /** | ||
| * POSTs file bytes to uploadUrl as multipart/form-data using the Node.js https module. | ||
| * All metadata is carried as URL query parameters; only the file bytes travel in the body. | ||
| */ | ||
| function sendFileUpload(context, uploadUrl, filename, fileData, callback) { | ||
| var boundary = '----BrainCloudS2SBoundary' + Date.now(); | ||
| var fileBuffer = Buffer.isBuffer(fileData) ? fileData : Buffer.from(fileData); | ||
| var headerPart = Buffer.from( | ||
| '--' + boundary + '\r\n' + | ||
| 'Content-Disposition: form-data; name="file"; filename="' + filename + '"\r\n' + | ||
| 'Content-Type: application/octet-stream\r\n\r\n' | ||
| ); | ||
| var footerPart = Buffer.from('\r\n--' + boundary + '--\r\n'); | ||
| var body = Buffer.concat([headerPart, fileBuffer, footerPart]); | ||
| var parsedUrl = new URL(uploadUrl); | ||
| var options = { | ||
| hostname: parsedUrl.hostname, | ||
| port: parsedUrl.port || 443, | ||
| path: parsedUrl.pathname + parsedUrl.search, | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'multipart/form-data; boundary=' + boundary, | ||
| 'Content-Length': body.length | ||
| } | ||
| }; | ||
| if (context.logEnabled) { | ||
| console.log('[GlobalFileV3] POST ' + options.hostname + options.path + | ||
| ' (' + body.length + ' bytes)'); | ||
| } | ||
| var req = https.request(options, (res) => { | ||
| var data = ''; | ||
| res.on('data', (chunk) => { data += chunk; }); | ||
| res.on('end', () => { | ||
| if (context.logEnabled) { | ||
| console.log('[GlobalFileV3] Upload response: ' + data); | ||
| } | ||
| var responseData = null; | ||
| try { responseData = JSON.parse(data); } catch (e) {} | ||
| if (callback) callback(context, responseData); | ||
| }); | ||
| }); | ||
| req.on('error', (err) => { | ||
| if (context.logEnabled) { | ||
| console.log('[GlobalFileV3] Upload error: ' + err.message); | ||
| } | ||
| if (callback) callback(context, { status: 900, status_message: 'File upload failed: ' + err.message }); | ||
| }); | ||
| req.write(body); | ||
| req.end(); | ||
| } |
| 'use strict'; | ||
| //---------------------------------------------------- | ||
| // brainCloud client source code | ||
| // Copyright 2026 bitHeads, inc. | ||
| //---------------------------------------------------- | ||
| var brainclouds2s = require('./brainclouds2s'); | ||
| /** | ||
| * Handles the Pre-Ready Launch (PRL) flow for custom servers launched by brainCloud. | ||
| * | ||
| * When PRE_READY_LAUNCH is "true", the server must wait for the assigned lobby to | ||
| * reach the "starting" state before proceeding with launch. | ||
| * | ||
| * Usage: | ||
| * const prl = require('./brainclouds2s-prl'); | ||
| * if (prl.isPreReadyLaunch()) { | ||
| * brainclouds2s.authenticate(context, (ctx, result) => { | ||
| * prl.start(context, lobbyId, (proceed) => { | ||
| * if (proceed) { ... } else { process.exit(0); } | ||
| * }); | ||
| * }); | ||
| * } | ||
| */ | ||
| /** | ||
| * Returns true if PRE_READY_LAUNCH environment variable is set to "true". | ||
| */ | ||
| exports.isPreReadyLaunch = () => { | ||
| var val = process.env['PRE_READY_LAUNCH']; | ||
| return val != null && val.toLowerCase() === 'true'; | ||
| }; | ||
| /** | ||
| * Returns the timeout in seconds from PRL_TIMEOUT_SECS or | ||
| * PRE_READY_LAUNCH_TIMEOUT_SECS environment variables. Defaults to 60. | ||
| */ | ||
| exports.getTimeoutSecs = () => { | ||
| var val = process.env['PRL_TIMEOUT_SECS']; | ||
| if (val) { var s = parseInt(val); if (!isNaN(s)) return s; } | ||
| val = process.env['PRE_READY_LAUNCH_TIMEOUT_SECS']; | ||
| if (val) { var s = parseInt(val); if (!isNaN(s)) return s; } | ||
| return 60; | ||
| }; | ||
| /** | ||
| * Parses the SERVER_CONTEXT environment variable into an object. | ||
| * Handles single-quoted and backslash-escaped JSON strings. | ||
| */ | ||
| exports.parseServerContext = () => { | ||
| try { | ||
| var val = process.env['SERVER_CONTEXT'] || '{}'; | ||
| val = val.trim().replace(/^'|'$/g, '').replace(/\\"/g, '"'); | ||
| return JSON.parse(val); | ||
| } catch (e) { | ||
| return {}; | ||
| } | ||
| }; | ||
| /** | ||
| * Builds the lobby RTT channel ID from the app ID and lobby ID. | ||
| * Lobby ID format: <appId>:<instanceId> | ||
| * Channel format: <appId>:sy:_lobby_<instanceId> | ||
| */ | ||
| function buildChannelId(appId, lobbyId) { | ||
| var instanceId = lobbyId; | ||
| var colonPos = lobbyId.indexOf(':'); | ||
| if (colonPos >= 0) instanceId = lobbyId.substring(colonPos + 1); | ||
| return appId + ':sy:_lobby_' + instanceId; | ||
| } | ||
| /** | ||
| * Parses the lobby state from a GET_LOBBY_DATA response. | ||
| * @param {object} result - S2S response object | ||
| * @returns {string|null} lobby state string, or null on failure | ||
| */ | ||
| function parseLobbyState(result) { | ||
| try { | ||
| if (!result || result.status !== 200) return null; | ||
| return result.data.state || null; | ||
| } catch (e) { return null; } | ||
| } | ||
| /** | ||
| * Parses the lobby state from an RTT push message. | ||
| * Expected format: { service: "chat", operation: "INCOMING", data: { content: { data: { lobby: { state: "..." } } } } } | ||
| * @param {object} msg - RTT message object | ||
| * @returns {string|null} lobby state string, or null if not a lobby state message | ||
| */ | ||
| function parseLobbyStateFromRTT(msg) { | ||
| try { | ||
| if (msg.service !== 'chat') return null; | ||
| if (msg.operation !== 'INCOMING') return null; | ||
| return msg.data.content.data.lobby.state || null; | ||
| } catch (e) { return null; } | ||
| } | ||
| /** | ||
| * Sends SYS_ROOM_SESSION_ENDED to notify brainCloud the server session has concluded. | ||
| * Should be called before the server process exits. | ||
| * @param {object} context - S2S context returned by brainclouds2s.init() | ||
| * @param {function} [callback] - Optional callback with signature (context, result) | ||
| */ | ||
| exports.sendSessionEnded = (context, callback) => { | ||
| console.log('[S2S PRL] Sending SYS_ROOM_SESSION_ENDED.'); | ||
| brainclouds2s.request(context, { | ||
| service: 'roomServer', | ||
| operation: 'SYS_ROOM_SESSION_ENDED', | ||
| data: { | ||
| serverId: process.env['SERVER_ID'], | ||
| serverContext: exports.parseServerContext() | ||
| } | ||
| }, callback || null); | ||
| }; | ||
| /** | ||
| * Starts the PRL flow. Assumes the S2S context is already authenticated. | ||
| * | ||
| * Flow: | ||
| * 1. Enable RTT | ||
| * 2. Subscribe to the lobby status channel (chat/CHANNEL_CONNECT) | ||
| * 3. Notify brainCloud the room session has started (roomServer/SYS_ROOM_SESSION_STARTED) | ||
| * 4. Query the lobby state (lobby/GET_LOBBY_DATA) | ||
| * 5. If state is "starting" → proceed. If "disbanded" or not found → exit. | ||
| * Otherwise wait for an RTT push with the state transition. | ||
| * | ||
| * @param {object} context - S2S context returned by brainclouds2s.init() | ||
| * @param {string} lobbyId - The lobby ID | ||
| * @param {function} callback - Called with (proceed: boolean) when PRL is complete | ||
| */ | ||
| exports.start = (context, lobbyId, callback) => { | ||
| var timeoutSecs = exports.getTimeoutSecs(); | ||
| var complete = false; | ||
| var timeoutId = null; | ||
| console.log('[S2S PRL] Starting PRL flow. lobbyId=' + lobbyId + ', timeoutSecs=' + timeoutSecs); | ||
| function done(proceed) { | ||
| if (complete) return; | ||
| complete = true; | ||
| if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } | ||
| brainclouds2s.deregisterRTTRawCallback(); | ||
| callback(proceed); | ||
| } | ||
| if (timeoutSecs > 0) { | ||
| timeoutId = setTimeout(() => { | ||
| console.log('[S2S PRL] Timeout elapsed — exiting.'); | ||
| done(false); | ||
| }, timeoutSecs * 1000); | ||
| } | ||
| function handleLobbyState(lobbyState) { | ||
| if (lobbyState === null || lobbyState === undefined) { | ||
| console.log('[S2S PRL] Lobby not found — exiting.'); | ||
| done(false); | ||
| } else if (lobbyState === 'disbanded') { | ||
| console.log('[S2S PRL] Lobby disbanded — exiting.'); | ||
| done(false); | ||
| } else if (lobbyState === 'starting') { | ||
| console.log('[S2S PRL] Lobby is starting — proceeding with launch.'); | ||
| done(true); | ||
| } else { | ||
| console.log('[S2S PRL] Lobby state is "' + lobbyState + '" — waiting for RTT update.'); | ||
| } | ||
| } | ||
| // Register RTT callback to receive lobby state push notifications | ||
| brainclouds2s.registerRTTRawCallback((msg) => { | ||
| console.log('[S2S PRL] RTT message: ' + JSON.stringify(msg)); | ||
| if (complete) return; | ||
| var lobbyState = parseLobbyStateFromRTT(msg); | ||
| if (lobbyState !== null) { | ||
| console.log('[S2S PRL] RTT lobby state update: ' + lobbyState); | ||
| handleLobbyState(lobbyState); | ||
| } | ||
| }); | ||
| // Step 1: Enable RTT | ||
| brainclouds2s.enableRTT(context, | ||
| (rttResult) => { | ||
| var channelId = buildChannelId(context.appId, lobbyId); | ||
| console.log('[S2S PRL] RTT connected. Subscribing to channel: ' + channelId); | ||
| // Step 2: Subscribe to the lobby status channel | ||
| brainclouds2s.request(context, { | ||
| service: 'chat', | ||
| operation: 'CHANNEL_CONNECT', | ||
| data: { | ||
| channelId: channelId, | ||
| maxReturn: 50 | ||
| } | ||
| }, (ctx, result) => { | ||
| if (!result || result.status !== 200) { | ||
| console.log('[S2S PRL] Failed to subscribe to channel: ' + JSON.stringify(result)); | ||
| done(false); | ||
| return; | ||
| } | ||
| console.log('[S2S PRL] Channel subscribed. Notifying session started.'); | ||
| // Step 3: Notify brainCloud the room session has started | ||
| var serverContext = exports.parseServerContext(); | ||
| console.log('[S2S PRL] SERVER_CONTEXT = ' + JSON.stringify(serverContext)); | ||
| brainclouds2s.request(context, { | ||
| service: 'roomServer', | ||
| operation: 'SYS_ROOM_SESSION_STARTED', | ||
| data: { | ||
| serverId: process.env['SERVER_ID'], | ||
| serverContext: serverContext | ||
| } | ||
| }, (ctx, result) => { | ||
| if (!result || result.status !== 200) { | ||
| console.log('[S2S PRL] SYS_ROOM_SESSION_STARTED failed: ' + JSON.stringify(result)); | ||
| done(false); | ||
| return; | ||
| } | ||
| console.log('[S2S PRL] Session started. Querying lobby state.'); | ||
| // Step 4: Query the current lobby state | ||
| brainclouds2s.request(context, { | ||
| service: 'lobby', | ||
| operation: 'GET_LOBBY_DATA', | ||
| data: { lobbyId: lobbyId } | ||
| }, (ctx, result) => { | ||
| var lobbyState = parseLobbyState(result); | ||
| console.log('[S2S PRL] Initial lobby state: ' + lobbyState); | ||
| handleLobbyState(lobbyState); | ||
| }); | ||
| }); | ||
| }); | ||
| }, | ||
| (err) => { | ||
| console.log('[S2S PRL] RTT connection failed: ' + JSON.stringify(err)); | ||
| done(false); | ||
| } | ||
| ); | ||
| }; |
+54
-36
@@ -0,3 +1,10 @@ | ||
| 'use strict'; | ||
| let S2S = require('./brainclouds2s.js'); | ||
| // Use the native WebSocket if available (Node.js 22+), otherwise fall back to ws | ||
| if (typeof WebSocket === 'undefined') { | ||
| var WebSocket = require('ws'); | ||
| } | ||
| var socket = null | ||
@@ -166,6 +173,13 @@ | ||
| if (typeof e.data === "string") { | ||
| processResult(e.data); | ||
| // String data (Node.js ws library or modern browsers) — parse as JSON | ||
| var parsed = {}; | ||
| try { | ||
| parsed = JSON.parse(e.data); | ||
| } catch (err) { | ||
| console.log("WS RECV parse error: " + err + " data=" + e.data); | ||
| return; | ||
| } | ||
| processResult(parsed); | ||
| } else if (typeof FileReader !== 'undefined') { | ||
| // Web Browser | ||
| // Web Browser — binary/blob data | ||
| var reader = new FileReader(); | ||
@@ -176,7 +190,6 @@ reader.onload = function () { | ||
| parsed = JSON.parse(reader.result); | ||
| } catch (err) { | ||
| console.log("WS RECV parse error: " + err + " data=" + reader.result); | ||
| return; | ||
| } | ||
| catch (e) { | ||
| console.log("WS RECV: " + reader.result); | ||
| parsed = JSON.parse(reader.result); // Trigger the error again and let it fail | ||
| } | ||
| processResult(parsed); | ||
@@ -186,12 +199,10 @@ } | ||
| } else { | ||
| // Node.js | ||
| // Fallback | ||
| var parsed = {}; | ||
| try { | ||
| parsed = JSON.parse(e.data); | ||
| } catch (err) { | ||
| console.log("WS RECV parse error: " + err + " data=" + e.data); | ||
| return; | ||
| } | ||
| catch (e) { | ||
| console.log("WS RECV: " + e.data); | ||
| parsed = JSON.parse(e.data); // Trigger the error again and let it fail | ||
| } | ||
| processResult(parsed); | ||
@@ -218,34 +229,41 @@ } | ||
| * Gets the name of the browser being used. | ||
| * @returns name of browser | ||
| * Returns null in non-browser environments (e.g. Node.js). | ||
| * @returns name of browser, or null | ||
| */ | ||
| function getBrowserName() { | ||
| // Opera 8.0+ | ||
| var isOpera = (!!window.opr && !!opr.addons) || !!window.opera || (typeof navigator !== 'undefined' && navigator.userAgent.indexOf(' OPR/') >= 0); | ||
| if (typeof window === 'undefined') { | ||
| return null; // Node.js environment | ||
| } | ||
| try { | ||
| // Opera 8.0+ | ||
| var isOpera = (!!window.opr && !!window.opr.addons) || !!window.opera || (typeof navigator !== 'undefined' && navigator.userAgent.indexOf(' OPR/') >= 0); | ||
| // Firefox 1.0+ | ||
| var isFirefox = typeof InstallTrigger !== 'undefined'; | ||
| // Firefox 1.0+ | ||
| var isFirefox = typeof InstallTrigger !== 'undefined'; | ||
| // Safari 3.0+ "[object HTMLElementConstructor]" | ||
| var isSafari = /constructor/i.test(window.HTMLElement) || (function (p) { return p.toString() === "[object SafariRemoteNotification]"; })(!window['safari'] || (typeof safari !== 'undefined' && safari.pushNotification)); | ||
| // Safari 3.0+ | ||
| var isSafari = /constructor/i.test(window.HTMLElement) || (function (p) { return p.toString() === "[object SafariRemoteNotification]"; })(!window['safari'] || (typeof safari !== 'undefined' && safari.pushNotification)); | ||
| // Internet Explorer 6-11 | ||
| var isIE = (typeof document !== 'undefined' && !!document.documentMode); | ||
| // Internet Explorer 6-11 | ||
| var isIE = (typeof document !== 'undefined' && !!document.documentMode); | ||
| // Edge 20+ | ||
| var isEdge = !isIE && !!window.StyleMedia; | ||
| // Edge 20+ | ||
| var isEdge = !isIE && !!window.StyleMedia; | ||
| // Chrome 1+ | ||
| var isChrome = !!window.chrome && !!window.chrome.webstore; | ||
| // Chrome 1+ | ||
| var isChrome = !!window.chrome && !!window.chrome.webstore; | ||
| // Blink engine detection | ||
| var isBlink = (isChrome || isOpera) && !!window.CSS; | ||
| // Blink engine detection | ||
| var isBlink = (isChrome || isOpera) && !!window.CSS; | ||
| if (isOpera) return "opera"; | ||
| if (isFirefox) return "firefox"; | ||
| if (isSafari) return "safari"; | ||
| if (isIE) return "ie"; | ||
| if (isEdge) return "edge"; | ||
| if (isChrome) return "chrome"; | ||
| if (isBlink) return "blink"; | ||
| if (isOpera) return "opera"; | ||
| if (isFirefox) return "firefox"; | ||
| if (isSafari) return "safari"; | ||
| if (isIE) return "ie"; | ||
| if (isEdge) return "edge"; | ||
| if (isChrome) return "chrome"; | ||
| if (isBlink) return "blink"; | ||
| } catch (e) { | ||
| // ignore | ||
| } | ||
| return null; | ||
@@ -252,0 +270,0 @@ } |
+1
-1
| { | ||
| "name": "brainclouds2s", | ||
| "version": "5.9.0", | ||
| "version": "6.0.0", | ||
| "description": "brianCloud S2S module", | ||
@@ -5,0 +5,0 @@ "main": "brainclouds2s.js", |
+243
-0
| const fs = require('fs') | ||
| let S2S = require('./brainclouds2s.js'); | ||
| let GFV3 = require('./brainclouds2s-globalfilev3.js'); | ||
@@ -527,5 +528,247 @@ /** | ||
| async function run_globalfilev3_tests() | ||
| { | ||
| if (!module("GlobalFileV3", null, null)) return; | ||
| // Shared state: captured from SysCreateFolder and UploadGlobalFile responses | ||
| var gfv3FolderTreeId = ""; | ||
| var gfv3FileId = ""; | ||
| var gfv3FileVersion = 1; | ||
| // Test 1 (parity: dotnet #8): SysGetGlobalFileList | ||
| await asyncTest("sysGetGlobalFileList", 2, () => | ||
| { | ||
| let s2s = S2S.init(GAME_ID, SERVER_NAME, SERVER_SECRET, S2S_URL, false); | ||
| S2S.authenticate(s2s, (s2s, result) => | ||
| { | ||
| equal(result && result.status, 200, "Auth: " + JSON.stringify(result)); | ||
| GFV3.sysGetGlobalFileList(s2s, "", true, (s2s, result) => | ||
| { | ||
| equal(result && result.status, 200, "SysGetGlobalFileList: " + JSON.stringify(result)); | ||
| resolve_test(); | ||
| }); | ||
| }); | ||
| }); | ||
| // Test 2 (parity: dotnet #9): SysLookupFolder | ||
| await asyncTest("sysLookupFolder", 2, () => | ||
| { | ||
| let s2s = S2S.init(GAME_ID, SERVER_NAME, SERVER_SECRET, S2S_URL, false); | ||
| S2S.authenticate(s2s, (s2s, result) => | ||
| { | ||
| equal(result && result.status, 200, "Auth: " + JSON.stringify(result)); | ||
| GFV3.sysLookupFolder(s2s, "s2s_test_folder", (s2s, result) => | ||
| { | ||
| equal(result && result.status, 200, "SysLookupFolder: " + JSON.stringify(result)); | ||
| resolve_test(); | ||
| }); | ||
| }); | ||
| }); | ||
| // Test 3 (parity: dotnet #10): SysCreateFolder — captures treeId | ||
| await asyncTest("sysCreateFolder", 2, () => | ||
| { | ||
| let s2s = S2S.init(GAME_ID, SERVER_NAME, SERVER_SECRET, S2S_URL, false); | ||
| S2S.authenticate(s2s, (s2s, result) => | ||
| { | ||
| equal(result && result.status, 200, "Auth: " + JSON.stringify(result)); | ||
| GFV3.sysCreateFolder(s2s, "s2s_test_folder", -1, "s2s_test_folder_2", | ||
| "S2S integration test folder", false, (s2s, result) => | ||
| { | ||
| if (result && result.status === 200 && result.data && result.data.createdTreeId) { | ||
| gfv3FolderTreeId = result.data.createdTreeId; | ||
| } | ||
| equal(result && result.status, 200, "SysCreateFolder: " + JSON.stringify(result)); | ||
| resolve_test(); | ||
| }); | ||
| }); | ||
| }); | ||
| // Test 4 (parity: dotnet #11): UploadGlobalFile — captures fileId and version | ||
| await asyncTest("uploadGlobalFile", 2, () => | ||
| { | ||
| let s2s = S2S.init(GAME_ID, SERVER_NAME, SERVER_SECRET, S2S_URL, false); | ||
| S2S.setLogEnabled(s2s, true); | ||
| S2S.authenticate(s2s, (s2s, result) => | ||
| { | ||
| equal(result && result.status, 200, "Auth: " + JSON.stringify(result)); | ||
| var fileData = Buffer.from("Hello from brainCloud S2S file upload test!"); | ||
| GFV3.uploadGlobalFile(s2s, gfv3FolderTreeId, "s2s_test_file.txt", true, fileData, | ||
| (s2s, result) => | ||
| { | ||
| if (result && result.status === 200 && | ||
| result.data && result.data.fileDetails && result.data.fileDetails.fileDetails) { | ||
| var fd = result.data.fileDetails.fileDetails; | ||
| gfv3FileId = fd.fileId || gfv3FileId; | ||
| gfv3FileVersion = fd.version || gfv3FileVersion; | ||
| } | ||
| equal(result && result.status, 200, "UploadGlobalFile: " + JSON.stringify(result)); | ||
| resolve_test(); | ||
| }); | ||
| }); | ||
| }); | ||
| // Test 5 (parity: dotnet #12): SysGetFileInfo | ||
| await asyncTest("sysGetFileInfo", 2, () => | ||
| { | ||
| let s2s = S2S.init(GAME_ID, SERVER_NAME, SERVER_SECRET, S2S_URL, false); | ||
| S2S.authenticate(s2s, (s2s, result) => | ||
| { | ||
| equal(result && result.status, 200, "Auth: " + JSON.stringify(result)); | ||
| GFV3.sysGetFileInfo(s2s, gfv3FileId, (s2s, result) => | ||
| { | ||
| equal(result && result.status, 200, "SysGetFileInfo: " + JSON.stringify(result)); | ||
| resolve_test(); | ||
| }); | ||
| }); | ||
| }); | ||
| // Test 6 (parity: dotnet #13): SysGetFileInfoSimple | ||
| await asyncTest("sysGetFileInfoSimple", 2, () => | ||
| { | ||
| let s2s = S2S.init(GAME_ID, SERVER_NAME, SERVER_SECRET, S2S_URL, false); | ||
| S2S.authenticate(s2s, (s2s, result) => | ||
| { | ||
| equal(result && result.status, 200, "Auth: " + JSON.stringify(result)); | ||
| GFV3.sysGetFileInfoSimple(s2s, "s2s_test_folder/s2s_test_folder_2", "s2s_test_file.txt", | ||
| (s2s, result) => | ||
| { | ||
| equal(result && result.status, 200, "SysGetFileInfoSimple: " + JSON.stringify(result)); | ||
| resolve_test(); | ||
| }); | ||
| }); | ||
| }); | ||
| // Test 7 (parity: dotnet #14): SysCheckFilenameExists | ||
| await asyncTest("sysCheckFilenameExists", 2, () => | ||
| { | ||
| let s2s = S2S.init(GAME_ID, SERVER_NAME, SERVER_SECRET, S2S_URL, false); | ||
| S2S.authenticate(s2s, (s2s, result) => | ||
| { | ||
| equal(result && result.status, 200, "Auth: " + JSON.stringify(result)); | ||
| GFV3.sysCheckFilenameExists(s2s, "s2s_test_folder/s2s_test_folder_2", "s2s_test_file.txt", | ||
| (s2s, result) => | ||
| { | ||
| equal(result && result.status, 200, "SysCheckFilenameExists: " + JSON.stringify(result)); | ||
| resolve_test(); | ||
| }); | ||
| }); | ||
| }); | ||
| // Test 8 (parity: dotnet #15): SysCheckFullpathFilenameExists | ||
| await asyncTest("sysCheckFullpathFilenameExists", 2, () => | ||
| { | ||
| let s2s = S2S.init(GAME_ID, SERVER_NAME, SERVER_SECRET, S2S_URL, false); | ||
| S2S.authenticate(s2s, (s2s, result) => | ||
| { | ||
| equal(result && result.status, 200, "Auth: " + JSON.stringify(result)); | ||
| GFV3.sysCheckFullpathFilenameExists(s2s, | ||
| "s2s_test_folder/s2s_test_folder_2/s2s_test_file.txt", (s2s, result) => | ||
| { | ||
| equal(result && result.status, 200, | ||
| "SysCheckFullpathFilenameExists: " + JSON.stringify(result)); | ||
| resolve_test(); | ||
| }); | ||
| }); | ||
| }); | ||
| // Test 9 (parity: dotnet #16): SysGetGlobalCDNUrl | ||
| await asyncTest("sysGetGlobalCDNUrl", 2, () => | ||
| { | ||
| let s2s = S2S.init(GAME_ID, SERVER_NAME, SERVER_SECRET, S2S_URL, false); | ||
| S2S.authenticate(s2s, (s2s, result) => | ||
| { | ||
| equal(result && result.status, 200, "Auth: " + JSON.stringify(result)); | ||
| GFV3.sysGetGlobalCDNUrl(s2s, gfv3FileId, (s2s, result) => | ||
| { | ||
| equal(result && result.status, 200, "SysGetGlobalCDNUrl: " + JSON.stringify(result)); | ||
| resolve_test(); | ||
| }); | ||
| }); | ||
| }); | ||
| // Test 10 (parity: dotnet #17): SysCopyGlobalFile | ||
| await asyncTest("sysCopyGlobalFile", 2, () => | ||
| { | ||
| let s2s = S2S.init(GAME_ID, SERVER_NAME, SERVER_SECRET, S2S_URL, false); | ||
| S2S.authenticate(s2s, (s2s, result) => | ||
| { | ||
| equal(result && result.status, 200, "Auth: " + JSON.stringify(result)); | ||
| GFV3.sysCopyGlobalFile(s2s, gfv3FileId, gfv3FileVersion, gfv3FolderTreeId, -1, | ||
| "s2s_file_copy.txt", true, (s2s, result) => | ||
| { | ||
| equal(result && result.status, 200, "SysCopyGlobalFile: " + JSON.stringify(result)); | ||
| resolve_test(); | ||
| }); | ||
| }); | ||
| }); | ||
| // Test 11 (parity: dotnet #18): SysMoveGlobalFile | ||
| await asyncTest("sysMoveGlobalFile", 2, () => | ||
| { | ||
| let s2s = S2S.init(GAME_ID, SERVER_NAME, SERVER_SECRET, S2S_URL, false); | ||
| S2S.authenticate(s2s, (s2s, result) => | ||
| { | ||
| equal(result && result.status, 200, "Auth: " + JSON.stringify(result)); | ||
| GFV3.sysMoveGlobalFile(s2s, gfv3FileId, gfv3FileVersion, gfv3FolderTreeId, -1, | ||
| "s2s_file_moved.txt", true, (s2s, result) => | ||
| { | ||
| equal(result && result.status, 200, "SysMoveGlobalFile: " + JSON.stringify(result)); | ||
| resolve_test(); | ||
| }); | ||
| }); | ||
| }); | ||
| // Test 12 (parity: dotnet #19): SysRenameFolder | ||
| await asyncTest("sysRenameFolder", 2, () => | ||
| { | ||
| let s2s = S2S.init(GAME_ID, SERVER_NAME, SERVER_SECRET, S2S_URL, false); | ||
| S2S.authenticate(s2s, (s2s, result) => | ||
| { | ||
| equal(result && result.status, 200, "Auth: " + JSON.stringify(result)); | ||
| GFV3.sysRenameFolder(s2s, gfv3FolderTreeId, -1, "s2s_test_folder_renamed", (s2s, result) => | ||
| { | ||
| equal(result && result.status, 200, "SysRenameFolder: " + JSON.stringify(result)); | ||
| resolve_test(); | ||
| }); | ||
| }); | ||
| }); | ||
| // Test 13 (parity: dotnet #20): SysDeleteGlobalFiles — cleanup files | ||
| await asyncTest("sysDeleteGlobalFiles", 2, () => | ||
| { | ||
| let s2s = S2S.init(GAME_ID, SERVER_NAME, SERVER_SECRET, S2S_URL, false); | ||
| S2S.authenticate(s2s, (s2s, result) => | ||
| { | ||
| equal(result && result.status, 200, "Auth: " + JSON.stringify(result)); | ||
| GFV3.sysDeleteGlobalFiles(s2s, gfv3FolderTreeId, | ||
| "s2s_test_folder/s2s_test_folder_renamed", -1, true, (s2s, result) => | ||
| { | ||
| equal(result && result.status, 200, "SysDeleteGlobalFiles: " + JSON.stringify(result)); | ||
| resolve_test(); | ||
| }); | ||
| }); | ||
| }); | ||
| // Test 14 (parity: dotnet #21): SysDeleteFolder — cleanup folder | ||
| await asyncTest("sysDeleteFolder", 2, () => | ||
| { | ||
| let s2s = S2S.init(GAME_ID, SERVER_NAME, SERVER_SECRET, S2S_URL, false); | ||
| S2S.authenticate(s2s, (s2s, result) => | ||
| { | ||
| equal(result && result.status, 200, "Auth: " + JSON.stringify(result)); | ||
| GFV3.sysDeleteFolder(s2s, gfv3FolderTreeId, | ||
| "s2s_test_folder/s2s_test_folder_renamed", -1, true, (s2s, result) => | ||
| { | ||
| equal(result && result.status, 200, "SysDeleteFolder: " + JSON.stringify(result)); | ||
| resolve_test(); | ||
| }); | ||
| }); | ||
| }); | ||
| } | ||
| async function main() | ||
| { | ||
| await run_tests(); | ||
| await run_globalfilev3_tests(); | ||
@@ -532,0 +775,0 @@ console.log(((test_passed === test_count) ? "\x1b[32m[PASSED] " : "\x1b[31m[FAILED] ") + test_passed + "/" + test_count + " passed\x1b[0m"); |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 5 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
90580
77.14%8
33.33%2027
81.63%8
300%2
100%