@hyve-sdk/js 2.13.0 → 2.14.0-canary.1
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/dist/index.d.mts +144 -2
- package/dist/index.d.ts +144 -2
- package/dist/index.js +411 -45
- package/dist/index.mjs +410 -45
- package/dist/react.d.mts +55 -1
- package/dist/react.d.ts +55 -1
- package/dist/react.js +409 -45
- package/dist/react.mjs +409 -45
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1201,6 +1201,7 @@ var PlaygamaService = class {
|
|
|
1201
1201
|
var CRAZYGAMES_SDK_CDN = "https://sdk.crazygames.com/crazygames-sdk-v2.js";
|
|
1202
1202
|
var CrazyGamesService = class {
|
|
1203
1203
|
initialized = false;
|
|
1204
|
+
environment = null;
|
|
1204
1205
|
/**
|
|
1205
1206
|
* Detects if the game is running on the CrazyGames platform.
|
|
1206
1207
|
* Games on CrazyGames run inside an iframe, so we check document.referrer
|
|
@@ -1242,6 +1243,7 @@ var CrazyGamesService = class {
|
|
|
1242
1243
|
logger.warn("[CrazyGamesService] Unexpected environment:", env);
|
|
1243
1244
|
return false;
|
|
1244
1245
|
}
|
|
1246
|
+
this.environment = env;
|
|
1245
1247
|
this.initialized = true;
|
|
1246
1248
|
return true;
|
|
1247
1249
|
} catch (error) {
|
|
@@ -1352,6 +1354,88 @@ var CrazyGamesService = class {
|
|
|
1352
1354
|
if (!this.initialized) return;
|
|
1353
1355
|
window.CrazyGames?.SDK.game.happytime();
|
|
1354
1356
|
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Returns the resolved CrazyGames environment ('crazygames' | 'local' |
|
|
1359
|
+
* 'disabled'), or null if the SDK has not initialized yet. Used to
|
|
1360
|
+
* auto-select the CrazyGames storage adapter.
|
|
1361
|
+
*/
|
|
1362
|
+
getEnvironment() {
|
|
1363
|
+
return this.environment;
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Whether the CrazyGames account system is present (sync). When false,
|
|
1367
|
+
* there is no logged-in user concept and telemetry falls back to guests.
|
|
1368
|
+
*/
|
|
1369
|
+
isUserAccountAvailable() {
|
|
1370
|
+
if (!this.initialized) return false;
|
|
1371
|
+
return window.CrazyGames?.SDK.user.isUserAccountAvailable ?? false;
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Fetches the current CrazyGames user, or null if the player is not logged
|
|
1375
|
+
* in (guest). The returned `__dangerousUserId` is client-asserted and used
|
|
1376
|
+
* only as a telemetry attribution label.
|
|
1377
|
+
*/
|
|
1378
|
+
async getUser() {
|
|
1379
|
+
const sdk = window.CrazyGames?.SDK;
|
|
1380
|
+
if (!this.initialized || !sdk || !this.isUserAccountAvailable()) {
|
|
1381
|
+
return null;
|
|
1382
|
+
}
|
|
1383
|
+
try {
|
|
1384
|
+
return await sdk.user.getUser() ?? null;
|
|
1385
|
+
} catch (error) {
|
|
1386
|
+
logger.warn("[CrazyGamesService] getUser failed:", error);
|
|
1387
|
+
return null;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Registers a listener that fires when a guest logs in mid-session.
|
|
1392
|
+
* No-op if the SDK or account system is unavailable.
|
|
1393
|
+
*/
|
|
1394
|
+
addAuthListener(callback) {
|
|
1395
|
+
const sdk = window.CrazyGames?.SDK;
|
|
1396
|
+
if (!this.initialized || !sdk || !this.isUserAccountAvailable()) return;
|
|
1397
|
+
try {
|
|
1398
|
+
sdk.user.addAuthListener(callback);
|
|
1399
|
+
} catch (error) {
|
|
1400
|
+
logger.warn("[CrazyGamesService] addAuthListener failed:", error);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
/** Removes a previously registered auth listener. */
|
|
1404
|
+
removeAuthListener(callback) {
|
|
1405
|
+
const sdk = window.CrazyGames?.SDK;
|
|
1406
|
+
if (!this.initialized || !sdk) return;
|
|
1407
|
+
try {
|
|
1408
|
+
sdk.user.removeAuthListener(callback);
|
|
1409
|
+
} catch (error) {
|
|
1410
|
+
logger.warn("[CrazyGamesService] removeAuthListener failed:", error);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Reads a value from the CrazyGames data store. Returns null when the SDK
|
|
1415
|
+
* is unavailable or the key is absent.
|
|
1416
|
+
*/
|
|
1417
|
+
async dataGetItem(key) {
|
|
1418
|
+
const sdk = window.CrazyGames?.SDK;
|
|
1419
|
+
if (!this.initialized || !sdk) return null;
|
|
1420
|
+
const value = await sdk.data.getItem(key);
|
|
1421
|
+
return value ?? null;
|
|
1422
|
+
}
|
|
1423
|
+
/** Writes a value to the CrazyGames data store. */
|
|
1424
|
+
async dataSetItem(key, value) {
|
|
1425
|
+
const sdk = window.CrazyGames?.SDK;
|
|
1426
|
+
if (!this.initialized || !sdk) {
|
|
1427
|
+
throw new Error("CrazyGames SDK not initialized");
|
|
1428
|
+
}
|
|
1429
|
+
await sdk.data.setItem(key, value);
|
|
1430
|
+
}
|
|
1431
|
+
/** Removes a value from the CrazyGames data store. */
|
|
1432
|
+
async dataRemoveItem(key) {
|
|
1433
|
+
const sdk = window.CrazyGames?.SDK;
|
|
1434
|
+
if (!this.initialized || !sdk) {
|
|
1435
|
+
throw new Error("CrazyGames SDK not initialized");
|
|
1436
|
+
}
|
|
1437
|
+
await sdk.data.removeItem(key);
|
|
1438
|
+
}
|
|
1355
1439
|
loadScript() {
|
|
1356
1440
|
return new Promise((resolve, reject) => {
|
|
1357
1441
|
if (window.CrazyGames?.SDK) {
|
|
@@ -2019,6 +2103,162 @@ var LocalStorageAdapter = class {
|
|
|
2019
2103
|
return count;
|
|
2020
2104
|
}
|
|
2021
2105
|
};
|
|
2106
|
+
function isGameDataObject(value) {
|
|
2107
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2108
|
+
}
|
|
2109
|
+
function toNumber(value) {
|
|
2110
|
+
return typeof value === "number" ? value : 0;
|
|
2111
|
+
}
|
|
2112
|
+
function applyOperation(operation, current, operand) {
|
|
2113
|
+
switch (operation) {
|
|
2114
|
+
case "set":
|
|
2115
|
+
return operand;
|
|
2116
|
+
case "add":
|
|
2117
|
+
return toNumber(current) + toNumber(operand);
|
|
2118
|
+
case "subtract":
|
|
2119
|
+
return toNumber(current) - toNumber(operand);
|
|
2120
|
+
case "multiply":
|
|
2121
|
+
return toNumber(current) * toNumber(operand);
|
|
2122
|
+
case "divide": {
|
|
2123
|
+
const divisor = toNumber(operand);
|
|
2124
|
+
if (divisor === 0) throw new Error("Cannot divide by zero");
|
|
2125
|
+
return toNumber(current) / divisor;
|
|
2126
|
+
}
|
|
2127
|
+
case "modulo": {
|
|
2128
|
+
const divisor = toNumber(operand);
|
|
2129
|
+
if (divisor === 0) throw new Error("Cannot modulo by zero");
|
|
2130
|
+
return toNumber(current) % divisor;
|
|
2131
|
+
}
|
|
2132
|
+
// For min/max, a missing current has nothing to compare against — adopt the operand.
|
|
2133
|
+
case "min":
|
|
2134
|
+
return typeof current === "number" ? Math.min(current, toNumber(operand)) : operand;
|
|
2135
|
+
case "max":
|
|
2136
|
+
return typeof current === "number" ? Math.max(current, toNumber(operand)) : operand;
|
|
2137
|
+
case "append": {
|
|
2138
|
+
const base = Array.isArray(current) ? [...current] : current === void 0 || current === null ? [] : [current];
|
|
2139
|
+
if (Array.isArray(operand)) {
|
|
2140
|
+
base.push(...operand);
|
|
2141
|
+
} else {
|
|
2142
|
+
base.push(operand);
|
|
2143
|
+
}
|
|
2144
|
+
return base;
|
|
2145
|
+
}
|
|
2146
|
+
default:
|
|
2147
|
+
return operand;
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
function getAtPath(value, path) {
|
|
2151
|
+
let current = value;
|
|
2152
|
+
for (const segment of path.split(".")) {
|
|
2153
|
+
if (!isGameDataObject(current)) return void 0;
|
|
2154
|
+
current = current[segment];
|
|
2155
|
+
}
|
|
2156
|
+
return current;
|
|
2157
|
+
}
|
|
2158
|
+
function setAtPath(value, path, newValue) {
|
|
2159
|
+
const segments = path.split(".");
|
|
2160
|
+
const root = isGameDataObject(value) ? { ...value } : {};
|
|
2161
|
+
let cursor = root;
|
|
2162
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
2163
|
+
const segment = segments[i];
|
|
2164
|
+
const existing = cursor[segment];
|
|
2165
|
+
const next = isGameDataObject(existing) ? { ...existing } : {};
|
|
2166
|
+
cursor[segment] = next;
|
|
2167
|
+
cursor = next;
|
|
2168
|
+
}
|
|
2169
|
+
cursor[segments[segments.length - 1]] = newValue;
|
|
2170
|
+
return root;
|
|
2171
|
+
}
|
|
2172
|
+
var CrazyGamesStorageAdapter = class {
|
|
2173
|
+
constructor(service) {
|
|
2174
|
+
this.service = service;
|
|
2175
|
+
}
|
|
2176
|
+
getStorageKey(gameId, key) {
|
|
2177
|
+
return `${gameId}:${key}`;
|
|
2178
|
+
}
|
|
2179
|
+
async saveGameData(gameId, key, value, operation, path) {
|
|
2180
|
+
const storageKey = this.getStorageKey(gameId, key);
|
|
2181
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2182
|
+
const existingRaw = await this.service.dataGetItem(storageKey);
|
|
2183
|
+
const existing = existingRaw ? JSON.parse(existingRaw) : null;
|
|
2184
|
+
const createdAt = existing?.created_at ?? now;
|
|
2185
|
+
const useOperation = operation !== void 0;
|
|
2186
|
+
let storedValue;
|
|
2187
|
+
let opResult;
|
|
2188
|
+
if (path) {
|
|
2189
|
+
const current = getAtPath(existing?.value, path);
|
|
2190
|
+
const next = useOperation ? applyOperation(operation, current, value) : value;
|
|
2191
|
+
storedValue = setAtPath(existing?.value, path, next);
|
|
2192
|
+
opResult = next;
|
|
2193
|
+
} else {
|
|
2194
|
+
const current = existing?.value;
|
|
2195
|
+
const next = useOperation ? applyOperation(operation, current, value) : value;
|
|
2196
|
+
storedValue = next;
|
|
2197
|
+
opResult = next;
|
|
2198
|
+
}
|
|
2199
|
+
const item = {
|
|
2200
|
+
key,
|
|
2201
|
+
value: storedValue,
|
|
2202
|
+
created_at: createdAt,
|
|
2203
|
+
updated_at: now
|
|
2204
|
+
};
|
|
2205
|
+
await this.service.dataSetItem(storageKey, JSON.stringify(item));
|
|
2206
|
+
const response = { success: true, message: "Data saved successfully" };
|
|
2207
|
+
if (useOperation && operation !== "set") {
|
|
2208
|
+
response.result = opResult;
|
|
2209
|
+
}
|
|
2210
|
+
return response;
|
|
2211
|
+
}
|
|
2212
|
+
async batchSaveGameData(gameId, items) {
|
|
2213
|
+
const results = [];
|
|
2214
|
+
let usedOperation = false;
|
|
2215
|
+
for (const item of items) {
|
|
2216
|
+
if (item.operation !== void 0) usedOperation = true;
|
|
2217
|
+
try {
|
|
2218
|
+
const saved = await this.saveGameData(gameId, item.key, item.value, item.operation, item.path);
|
|
2219
|
+
results.push({ key: item.key, success: true, ...saved.result !== void 0 && { result: saved.result } });
|
|
2220
|
+
} catch (error) {
|
|
2221
|
+
results.push({
|
|
2222
|
+
key: item.key,
|
|
2223
|
+
success: false,
|
|
2224
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
const allSucceeded = results.every((r) => r.success);
|
|
2229
|
+
return {
|
|
2230
|
+
success: allSucceeded,
|
|
2231
|
+
message: `${results.filter((r) => r.success).length}/${items.length} items saved`,
|
|
2232
|
+
...usedOperation && { results }
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
async getGameData(gameId, key) {
|
|
2236
|
+
const raw = await this.service.dataGetItem(this.getStorageKey(gameId, key));
|
|
2237
|
+
return raw ? JSON.parse(raw) : null;
|
|
2238
|
+
}
|
|
2239
|
+
async getMultipleGameData(gameId, keys) {
|
|
2240
|
+
const items = [];
|
|
2241
|
+
for (const key of keys) {
|
|
2242
|
+
const item = await this.getGameData(gameId, key);
|
|
2243
|
+
if (item) items.push(item);
|
|
2244
|
+
}
|
|
2245
|
+
return items;
|
|
2246
|
+
}
|
|
2247
|
+
async deleteGameData(gameId, key) {
|
|
2248
|
+
const storageKey = this.getStorageKey(gameId, key);
|
|
2249
|
+
const existing = await this.service.dataGetItem(storageKey);
|
|
2250
|
+
if (existing === null) return false;
|
|
2251
|
+
await this.service.dataRemoveItem(storageKey);
|
|
2252
|
+
return true;
|
|
2253
|
+
}
|
|
2254
|
+
async deleteMultipleGameData(gameId, keys) {
|
|
2255
|
+
let count = 0;
|
|
2256
|
+
for (const key of keys) {
|
|
2257
|
+
if (await this.deleteGameData(gameId, key)) count++;
|
|
2258
|
+
}
|
|
2259
|
+
return count;
|
|
2260
|
+
}
|
|
2261
|
+
};
|
|
2022
2262
|
|
|
2023
2263
|
// src/core/client.ts
|
|
2024
2264
|
function determineEnvironmentFromParentUrl() {
|
|
@@ -2065,6 +2305,11 @@ var HyveClient = class {
|
|
|
2065
2305
|
storageMode;
|
|
2066
2306
|
cloudStorageAdapter;
|
|
2067
2307
|
localStorageAdapter;
|
|
2308
|
+
crazyGamesStorageAdapter = null;
|
|
2309
|
+
partnerApiKey = null;
|
|
2310
|
+
partnerApiBaseUrl = null;
|
|
2311
|
+
/** Raw (unprefixed) CrazyGames __dangerousUserId, seeded at init and on login. */
|
|
2312
|
+
crazyGamesUserId = null;
|
|
2068
2313
|
/**
|
|
2069
2314
|
* Creates a new HyveClient instance
|
|
2070
2315
|
* @param config Optional configuration including telemetry and ads
|
|
@@ -2092,10 +2337,17 @@ var HyveClient = class {
|
|
|
2092
2337
|
return success;
|
|
2093
2338
|
});
|
|
2094
2339
|
}
|
|
2340
|
+
this.partnerApiKey = config?.partnerApiKey ?? null;
|
|
2341
|
+
this.partnerApiBaseUrl = config?.partnerApiBaseUrl ?? null;
|
|
2342
|
+
this.gameId = config?.gameId ?? null;
|
|
2095
2343
|
if (typeof window !== "undefined" && CrazyGamesService.isCrazyGamesDomain()) {
|
|
2096
2344
|
this.crazyGamesService = new CrazyGamesService();
|
|
2097
|
-
this.
|
|
2345
|
+
this.crazyGamesStorageAdapter = new CrazyGamesStorageAdapter(this.crazyGamesService);
|
|
2346
|
+
this.crazyGamesInitPromise = this.crazyGamesService.initialize().then(async (success) => {
|
|
2098
2347
|
logger.info("CrazyGames SDK initialized:", success);
|
|
2348
|
+
if (success) {
|
|
2349
|
+
await this.seedCrazyGamesUser();
|
|
2350
|
+
}
|
|
2099
2351
|
return success;
|
|
2100
2352
|
});
|
|
2101
2353
|
}
|
|
@@ -2132,6 +2384,7 @@ var HyveClient = class {
|
|
|
2132
2384
|
!!config?.billing && Object.keys(config.billing).length > 0
|
|
2133
2385
|
);
|
|
2134
2386
|
logger.info("Storage mode:", this.storageMode);
|
|
2387
|
+
logger.info("Game ID:", this.gameId ?? "not set");
|
|
2135
2388
|
logger.info("Authenticated:", this.jwtToken !== null);
|
|
2136
2389
|
logger.debug("Config:", {
|
|
2137
2390
|
isDev: this.telemetryConfig.isDev,
|
|
@@ -2191,6 +2444,16 @@ var HyveClient = class {
|
|
|
2191
2444
|
* @returns Promise resolving to boolean indicating success
|
|
2192
2445
|
*/
|
|
2193
2446
|
async sendTelemetry(eventLocation, eventCategory, eventAction, eventSubCategory, eventSubAction, eventDetails, platformId) {
|
|
2447
|
+
if (this.partnerApiKey) {
|
|
2448
|
+
return this.sendPartnerTelemetry(
|
|
2449
|
+
eventLocation,
|
|
2450
|
+
eventCategory,
|
|
2451
|
+
eventAction,
|
|
2452
|
+
eventSubCategory,
|
|
2453
|
+
eventSubAction,
|
|
2454
|
+
eventDetails
|
|
2455
|
+
);
|
|
2456
|
+
}
|
|
2194
2457
|
if (!this.jwtToken) {
|
|
2195
2458
|
logger.error("JWT token required. Ensure hyve-access and game-id are present in the URL.");
|
|
2196
2459
|
return false;
|
|
@@ -2200,42 +2463,11 @@ var HyveClient = class {
|
|
|
2200
2463
|
return false;
|
|
2201
2464
|
}
|
|
2202
2465
|
try {
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
JSON.parse(eventDetails);
|
|
2207
|
-
} else if (typeof eventDetails === "object") {
|
|
2208
|
-
JSON.stringify(eventDetails);
|
|
2209
|
-
}
|
|
2210
|
-
} catch (validationError) {
|
|
2211
|
-
logger.error("Invalid JSON in eventDetails:", validationError);
|
|
2212
|
-
logger.error("eventDetails value:", eventDetails);
|
|
2213
|
-
return false;
|
|
2214
|
-
}
|
|
2215
|
-
}
|
|
2216
|
-
let parsedEventDetails = {};
|
|
2217
|
-
if (eventDetails) {
|
|
2218
|
-
if (typeof eventDetails === "string") {
|
|
2219
|
-
try {
|
|
2220
|
-
parsedEventDetails = JSON.parse(eventDetails);
|
|
2221
|
-
} catch {
|
|
2222
|
-
}
|
|
2223
|
-
} else {
|
|
2224
|
-
parsedEventDetails = eventDetails;
|
|
2225
|
-
}
|
|
2466
|
+
const enrichedEventDetails = this.enrichEventDetails(eventDetails);
|
|
2467
|
+
if (enrichedEventDetails === null) {
|
|
2468
|
+
return false;
|
|
2226
2469
|
}
|
|
2227
2470
|
const attribution = getAttributionData();
|
|
2228
|
-
const enrichedEventDetails = {
|
|
2229
|
-
// Device info
|
|
2230
|
-
...getEssentialDeviceInfo(),
|
|
2231
|
-
// Attribution data
|
|
2232
|
-
...attribution,
|
|
2233
|
-
// Timestamp and session context
|
|
2234
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2235
|
-
session_id: this.sessionId,
|
|
2236
|
-
// Event-specific details can override any of the above
|
|
2237
|
-
...parsedEventDetails
|
|
2238
|
-
};
|
|
2239
2471
|
const telemetryEvent = {
|
|
2240
2472
|
game_id: this.gameId,
|
|
2241
2473
|
session_id: this.sessionId,
|
|
@@ -2274,6 +2506,122 @@ var HyveClient = class {
|
|
|
2274
2506
|
return false;
|
|
2275
2507
|
}
|
|
2276
2508
|
}
|
|
2509
|
+
/**
|
|
2510
|
+
* Validates and enriches user-provided event details with device info,
|
|
2511
|
+
* attribution data, and session context — matching the enrichment
|
|
2512
|
+
* platform-v2 applies in sendAnalyticsEvent. Shared by the JWT and partner
|
|
2513
|
+
* telemetry paths.
|
|
2514
|
+
* @returns Enriched details object, or null if eventDetails is not valid JSON.
|
|
2515
|
+
*/
|
|
2516
|
+
enrichEventDetails(eventDetails) {
|
|
2517
|
+
let parsedEventDetails = {};
|
|
2518
|
+
if (eventDetails) {
|
|
2519
|
+
try {
|
|
2520
|
+
if (typeof eventDetails === "string") {
|
|
2521
|
+
parsedEventDetails = JSON.parse(eventDetails);
|
|
2522
|
+
} else if (typeof eventDetails === "object") {
|
|
2523
|
+
JSON.stringify(eventDetails);
|
|
2524
|
+
parsedEventDetails = eventDetails;
|
|
2525
|
+
}
|
|
2526
|
+
} catch (validationError) {
|
|
2527
|
+
logger.error("Invalid JSON in eventDetails:", validationError);
|
|
2528
|
+
logger.error("eventDetails value:", eventDetails);
|
|
2529
|
+
return null;
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
const attribution = getAttributionData();
|
|
2533
|
+
return {
|
|
2534
|
+
// Device info
|
|
2535
|
+
...getEssentialDeviceInfo(),
|
|
2536
|
+
// Attribution data
|
|
2537
|
+
...attribution,
|
|
2538
|
+
// Timestamp and session context
|
|
2539
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2540
|
+
session_id: this.sessionId,
|
|
2541
|
+
// Event-specific details can override any of the above
|
|
2542
|
+
...parsedEventDetails
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
/**
|
|
2546
|
+
* Sends a telemetry event via the partner analytics endpoint using a partner
|
|
2547
|
+
* API key (x-api-key) instead of a hyve JWT. Used by externally-hosted games
|
|
2548
|
+
* such as CrazyGames. game_id is derived from the API key server-side and is
|
|
2549
|
+
* therefore not sent. hyve_user_id carries the `cg:`-prefixed CrazyGames id,
|
|
2550
|
+
* or a guest sentinel when the player is not logged in.
|
|
2551
|
+
*/
|
|
2552
|
+
async sendPartnerTelemetry(eventLocation, eventCategory, eventAction, eventSubCategory, eventSubAction, eventDetails) {
|
|
2553
|
+
if (!this.partnerApiKey) {
|
|
2554
|
+
logger.error("Partner API key required for partner telemetry path.");
|
|
2555
|
+
return false;
|
|
2556
|
+
}
|
|
2557
|
+
if (this.crazyGamesService && this.crazyGamesInitPromise) {
|
|
2558
|
+
await this.crazyGamesInitPromise;
|
|
2559
|
+
}
|
|
2560
|
+
try {
|
|
2561
|
+
const enrichedEventDetails = this.enrichEventDetails(eventDetails);
|
|
2562
|
+
if (enrichedEventDetails === null) {
|
|
2563
|
+
return false;
|
|
2564
|
+
}
|
|
2565
|
+
const hyveUserId = this.crazyGamesUserId ? `cg:${this.crazyGamesUserId}` : "cg:guest";
|
|
2566
|
+
const partnerEvent = {
|
|
2567
|
+
session_id: this.sessionId,
|
|
2568
|
+
hyve_user_id: hyveUserId,
|
|
2569
|
+
event_location: eventLocation,
|
|
2570
|
+
event_category: eventCategory,
|
|
2571
|
+
event_sub_category: eventSubCategory || null,
|
|
2572
|
+
event_action: eventAction,
|
|
2573
|
+
event_sub_action: eventSubAction || null,
|
|
2574
|
+
event_details: enrichedEventDetails
|
|
2575
|
+
};
|
|
2576
|
+
logger.debug("Sending partner telemetry event:", partnerEvent);
|
|
2577
|
+
const base = this.partnerApiBaseUrl || this.apiBaseUrl;
|
|
2578
|
+
const telemetryUrl = `${base}/api/v1/partners/analytics/events`;
|
|
2579
|
+
const response = await fetch(telemetryUrl, {
|
|
2580
|
+
method: "POST",
|
|
2581
|
+
headers: {
|
|
2582
|
+
"Content-Type": "application/json",
|
|
2583
|
+
"x-api-key": this.partnerApiKey
|
|
2584
|
+
},
|
|
2585
|
+
body: JSON.stringify(partnerEvent)
|
|
2586
|
+
});
|
|
2587
|
+
if (response.ok) {
|
|
2588
|
+
logger.info("Partner telemetry event sent successfully:", response.status);
|
|
2589
|
+
return true;
|
|
2590
|
+
} else {
|
|
2591
|
+
const errorText = await response.text();
|
|
2592
|
+
logger.error(
|
|
2593
|
+
"Failed to send partner telemetry event:",
|
|
2594
|
+
response.status,
|
|
2595
|
+
errorText
|
|
2596
|
+
);
|
|
2597
|
+
return false;
|
|
2598
|
+
}
|
|
2599
|
+
} catch (error) {
|
|
2600
|
+
logger.error("Error sending partner telemetry event:", error);
|
|
2601
|
+
return false;
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
/**
|
|
2605
|
+
* Seeds the CrazyGames user id used for telemetry attribution and registers
|
|
2606
|
+
* an auth listener so the id updates if a guest logs in mid-session.
|
|
2607
|
+
* The id is client-asserted (spoofable) and used only as an attribution label.
|
|
2608
|
+
*/
|
|
2609
|
+
async seedCrazyGamesUser() {
|
|
2610
|
+
if (!this.crazyGamesService) return;
|
|
2611
|
+
if (!this.crazyGamesService.isUserAccountAvailable()) {
|
|
2612
|
+
logger.info("CrazyGames account system unavailable \u2014 telemetry attributed by session only");
|
|
2613
|
+
return;
|
|
2614
|
+
}
|
|
2615
|
+
const user = await this.crazyGamesService.getUser();
|
|
2616
|
+
this.crazyGamesUserId = user?.__dangerousUserId ?? null;
|
|
2617
|
+
if (this.crazyGamesUserId) {
|
|
2618
|
+
logger.info("CrazyGames user id seeded for telemetry attribution");
|
|
2619
|
+
}
|
|
2620
|
+
this.crazyGamesService.addAuthListener((updatedUser) => {
|
|
2621
|
+
this.crazyGamesUserId = updatedUser?.__dangerousUserId ?? null;
|
|
2622
|
+
logger.info("CrazyGames auth state changed; telemetry user id updated");
|
|
2623
|
+
});
|
|
2624
|
+
}
|
|
2277
2625
|
/**
|
|
2278
2626
|
* Required lifecycle telemetry — Session start.
|
|
2279
2627
|
* See https://docs.hyve.gg/docs/telemetry#required-lifecycle-events
|
|
@@ -2494,12 +2842,28 @@ var HyveClient = class {
|
|
|
2494
2842
|
logger.info("Client reset with new sessionId:", this.sessionId);
|
|
2495
2843
|
}
|
|
2496
2844
|
/**
|
|
2497
|
-
* Get the storage adapter based on mode
|
|
2845
|
+
* Get the storage adapter based on mode.
|
|
2846
|
+
*
|
|
2847
|
+
* Selection order:
|
|
2848
|
+
* 1. An explicit `mode` override ('cloud' | 'local') always wins.
|
|
2849
|
+
* 2. On the CrazyGames platform, the CrazyGames data store is auto-selected
|
|
2850
|
+
* (D3 — public storageMode type is preserved; this is chosen internally).
|
|
2851
|
+
* 3. Otherwise the configured storageMode is used.
|
|
2852
|
+
*
|
|
2853
|
+
* Awaits CrazyGames SDK initialization so the data store is ready before use.
|
|
2498
2854
|
* @param mode Storage mode override (cloud or local)
|
|
2499
2855
|
*/
|
|
2500
|
-
getStorageAdapter(mode) {
|
|
2501
|
-
|
|
2502
|
-
|
|
2856
|
+
async getStorageAdapter(mode) {
|
|
2857
|
+
if (mode) {
|
|
2858
|
+
return mode === "local" ? this.localStorageAdapter : this.cloudStorageAdapter;
|
|
2859
|
+
}
|
|
2860
|
+
if (this.crazyGamesService && this.crazyGamesStorageAdapter) {
|
|
2861
|
+
if (this.crazyGamesInitPromise) await this.crazyGamesInitPromise;
|
|
2862
|
+
if (this.crazyGamesService.getEnvironment() === "crazygames") {
|
|
2863
|
+
return this.crazyGamesStorageAdapter;
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
return this.storageMode === "local" ? this.localStorageAdapter : this.cloudStorageAdapter;
|
|
2503
2867
|
}
|
|
2504
2868
|
/**
|
|
2505
2869
|
* Returns the current game ID or throws if not available.
|
|
@@ -2523,7 +2887,7 @@ var HyveClient = class {
|
|
|
2523
2887
|
const gameId = this.requireGameId();
|
|
2524
2888
|
const storageMode = storage || this.storageMode;
|
|
2525
2889
|
logger.debug(`Saving game data to ${storageMode}: ${gameId}/${key}`);
|
|
2526
|
-
const adapter = this.getStorageAdapter(storage);
|
|
2890
|
+
const adapter = await this.getStorageAdapter(storage);
|
|
2527
2891
|
const response = await adapter.saveGameData(gameId, key, value, operation, path);
|
|
2528
2892
|
logger.info(`Game data saved successfully to ${storageMode}: ${gameId}/${key}`);
|
|
2529
2893
|
return response;
|
|
@@ -2538,7 +2902,7 @@ var HyveClient = class {
|
|
|
2538
2902
|
const gameId = this.requireGameId();
|
|
2539
2903
|
const storageMode = storage || this.storageMode;
|
|
2540
2904
|
logger.debug(`Batch saving ${items.length} game data entries to ${storageMode} for game: ${gameId}`);
|
|
2541
|
-
const adapter = this.getStorageAdapter(storage);
|
|
2905
|
+
const adapter = await this.getStorageAdapter(storage);
|
|
2542
2906
|
const response = await adapter.batchSaveGameData(gameId, items);
|
|
2543
2907
|
logger.info(`Batch saved ${items.length} game data entries successfully to ${storageMode}`);
|
|
2544
2908
|
return response;
|
|
@@ -2553,7 +2917,7 @@ var HyveClient = class {
|
|
|
2553
2917
|
const gameId = this.requireGameId();
|
|
2554
2918
|
const storageMode = storage || this.storageMode;
|
|
2555
2919
|
logger.debug(`Getting game data from ${storageMode}: ${gameId}/${key}`);
|
|
2556
|
-
const adapter = this.getStorageAdapter(storage);
|
|
2920
|
+
const adapter = await this.getStorageAdapter(storage);
|
|
2557
2921
|
const data = await adapter.getGameData(gameId, key);
|
|
2558
2922
|
if (data) {
|
|
2559
2923
|
logger.info(`Game data retrieved successfully from ${storageMode}: ${gameId}/${key}`);
|
|
@@ -2572,7 +2936,7 @@ var HyveClient = class {
|
|
|
2572
2936
|
const gameId = this.requireGameId();
|
|
2573
2937
|
const storageMode = storage || this.storageMode;
|
|
2574
2938
|
logger.debug(`Getting ${keys.length} game data entries from ${storageMode} for game: ${gameId}`);
|
|
2575
|
-
const adapter = this.getStorageAdapter(storage);
|
|
2939
|
+
const adapter = await this.getStorageAdapter(storage);
|
|
2576
2940
|
const data = await adapter.getMultipleGameData(gameId, keys);
|
|
2577
2941
|
logger.info(`Retrieved ${data.length} game data entries from ${storageMode}`);
|
|
2578
2942
|
return data;
|
|
@@ -2587,7 +2951,7 @@ var HyveClient = class {
|
|
|
2587
2951
|
const gameId = this.requireGameId();
|
|
2588
2952
|
const storageMode = storage || this.storageMode;
|
|
2589
2953
|
logger.debug(`Deleting game data from ${storageMode}: ${gameId}/${key}`);
|
|
2590
|
-
const adapter = this.getStorageAdapter(storage);
|
|
2954
|
+
const adapter = await this.getStorageAdapter(storage);
|
|
2591
2955
|
const deleted = await adapter.deleteGameData(gameId, key);
|
|
2592
2956
|
if (deleted) {
|
|
2593
2957
|
logger.info(`Game data deleted successfully from ${storageMode}: ${gameId}/${key}`);
|
|
@@ -2606,7 +2970,7 @@ var HyveClient = class {
|
|
|
2606
2970
|
const gameId = this.requireGameId();
|
|
2607
2971
|
const storageMode = storage || this.storageMode;
|
|
2608
2972
|
logger.debug(`Deleting ${keys.length} game data entries from ${storageMode} for game: ${gameId}`);
|
|
2609
|
-
const adapter = this.getStorageAdapter(storage);
|
|
2973
|
+
const adapter = await this.getStorageAdapter(storage);
|
|
2610
2974
|
const deletedCount = await adapter.deleteMultipleGameData(gameId, keys);
|
|
2611
2975
|
logger.info(`Deleted ${deletedCount} game data entries from ${storageMode}`);
|
|
2612
2976
|
return deletedCount;
|
|
@@ -2824,6 +3188,7 @@ export {
|
|
|
2824
3188
|
BillingService,
|
|
2825
3189
|
CloudStorageAdapter,
|
|
2826
3190
|
CrazyGamesService,
|
|
3191
|
+
CrazyGamesStorageAdapter,
|
|
2827
3192
|
HyveClient,
|
|
2828
3193
|
LocalStorageAdapter,
|
|
2829
3194
|
Logger,
|
package/dist/react.d.mts
CHANGED
|
@@ -256,6 +256,25 @@ interface HyveClientConfig extends TelemetryConfig {
|
|
|
256
256
|
billing?: BillingConfig;
|
|
257
257
|
/** Storage mode for persistent game data - 'cloud' (default) or 'local' */
|
|
258
258
|
storageMode?: 'cloud' | 'local';
|
|
259
|
+
/**
|
|
260
|
+
* Partner API key for externally-hosted games (e.g. CrazyGames) that cannot
|
|
261
|
+
* carry a hyve JWT. When set, telemetry is sent to the partner analytics
|
|
262
|
+
* endpoint with an `x-api-key` header instead of the JWT path. The key is
|
|
263
|
+
* scoped to the `analytics` domain and bound to a game_id server-side.
|
|
264
|
+
* NOTE: this key ships in client-side game code and is therefore public.
|
|
265
|
+
*/
|
|
266
|
+
partnerApiKey?: string;
|
|
267
|
+
/**
|
|
268
|
+
* Optional base URL for partner analytics requests. Defaults to apiBaseUrl
|
|
269
|
+
* when omitted.
|
|
270
|
+
*/
|
|
271
|
+
partnerApiBaseUrl?: string;
|
|
272
|
+
/**
|
|
273
|
+
* Game ID for externally-hosted builds (e.g. CrazyGames) whose URL carries
|
|
274
|
+
* no `game-id` parameter or hyve JWT. Used for storage key namespacing and
|
|
275
|
+
* telemetry. A `game-id` URL parameter still takes precedence when present.
|
|
276
|
+
*/
|
|
277
|
+
gameId?: string;
|
|
259
278
|
}
|
|
260
279
|
/**
|
|
261
280
|
* HyveClient provides telemetry and authentication functionality for Hyve games
|
|
@@ -276,6 +295,11 @@ declare class HyveClient {
|
|
|
276
295
|
private storageMode;
|
|
277
296
|
private cloudStorageAdapter;
|
|
278
297
|
private localStorageAdapter;
|
|
298
|
+
private crazyGamesStorageAdapter;
|
|
299
|
+
private partnerApiKey;
|
|
300
|
+
private partnerApiBaseUrl;
|
|
301
|
+
/** Raw (unprefixed) CrazyGames __dangerousUserId, seeded at init and on login. */
|
|
302
|
+
private crazyGamesUserId;
|
|
279
303
|
/**
|
|
280
304
|
* Creates a new HyveClient instance
|
|
281
305
|
* @param config Optional configuration including telemetry and ads
|
|
@@ -299,6 +323,28 @@ declare class HyveClient {
|
|
|
299
323
|
* @returns Promise resolving to boolean indicating success
|
|
300
324
|
*/
|
|
301
325
|
sendTelemetry(eventLocation: string, eventCategory: string, eventAction: string, eventSubCategory?: string | null, eventSubAction?: string | null, eventDetails?: Record<string, any> | string | null, platformId?: string | null): Promise<boolean>;
|
|
326
|
+
/**
|
|
327
|
+
* Validates and enriches user-provided event details with device info,
|
|
328
|
+
* attribution data, and session context — matching the enrichment
|
|
329
|
+
* platform-v2 applies in sendAnalyticsEvent. Shared by the JWT and partner
|
|
330
|
+
* telemetry paths.
|
|
331
|
+
* @returns Enriched details object, or null if eventDetails is not valid JSON.
|
|
332
|
+
*/
|
|
333
|
+
private enrichEventDetails;
|
|
334
|
+
/**
|
|
335
|
+
* Sends a telemetry event via the partner analytics endpoint using a partner
|
|
336
|
+
* API key (x-api-key) instead of a hyve JWT. Used by externally-hosted games
|
|
337
|
+
* such as CrazyGames. game_id is derived from the API key server-side and is
|
|
338
|
+
* therefore not sent. hyve_user_id carries the `cg:`-prefixed CrazyGames id,
|
|
339
|
+
* or a guest sentinel when the player is not logged in.
|
|
340
|
+
*/
|
|
341
|
+
private sendPartnerTelemetry;
|
|
342
|
+
/**
|
|
343
|
+
* Seeds the CrazyGames user id used for telemetry attribution and registers
|
|
344
|
+
* an auth listener so the id updates if a guest logs in mid-session.
|
|
345
|
+
* The id is client-asserted (spoofable) and used only as an attribution label.
|
|
346
|
+
*/
|
|
347
|
+
private seedCrazyGamesUser;
|
|
302
348
|
/**
|
|
303
349
|
* Required lifecycle telemetry — Session start.
|
|
304
350
|
* See https://docs.hyve.gg/docs/telemetry#required-lifecycle-events
|
|
@@ -419,7 +465,15 @@ declare class HyveClient {
|
|
|
419
465
|
*/
|
|
420
466
|
reset(): void;
|
|
421
467
|
/**
|
|
422
|
-
* Get the storage adapter based on mode
|
|
468
|
+
* Get the storage adapter based on mode.
|
|
469
|
+
*
|
|
470
|
+
* Selection order:
|
|
471
|
+
* 1. An explicit `mode` override ('cloud' | 'local') always wins.
|
|
472
|
+
* 2. On the CrazyGames platform, the CrazyGames data store is auto-selected
|
|
473
|
+
* (D3 — public storageMode type is preserved; this is chosen internally).
|
|
474
|
+
* 3. Otherwise the configured storageMode is used.
|
|
475
|
+
*
|
|
476
|
+
* Awaits CrazyGames SDK initialization so the data store is ready before use.
|
|
423
477
|
* @param mode Storage mode override (cloud or local)
|
|
424
478
|
*/
|
|
425
479
|
private getStorageAdapter;
|