@hyve-sdk/js 2.13.0 → 2.14.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +138 -2
- package/dist/index.d.ts +138 -2
- package/dist/index.js +409 -45
- package/dist/index.mjs +408 -45
- package/dist/react.d.mts +49 -1
- package/dist/react.d.ts +49 -1
- package/dist/react.js +407 -45
- package/dist/react.mjs +407 -45
- package/package.json +1 -1
package/dist/react.d.ts
CHANGED
|
@@ -256,6 +256,19 @@ 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;
|
|
259
272
|
}
|
|
260
273
|
/**
|
|
261
274
|
* HyveClient provides telemetry and authentication functionality for Hyve games
|
|
@@ -276,6 +289,11 @@ declare class HyveClient {
|
|
|
276
289
|
private storageMode;
|
|
277
290
|
private cloudStorageAdapter;
|
|
278
291
|
private localStorageAdapter;
|
|
292
|
+
private crazyGamesStorageAdapter;
|
|
293
|
+
private partnerApiKey;
|
|
294
|
+
private partnerApiBaseUrl;
|
|
295
|
+
/** Raw (unprefixed) CrazyGames __dangerousUserId, seeded at init and on login. */
|
|
296
|
+
private crazyGamesUserId;
|
|
279
297
|
/**
|
|
280
298
|
* Creates a new HyveClient instance
|
|
281
299
|
* @param config Optional configuration including telemetry and ads
|
|
@@ -299,6 +317,28 @@ declare class HyveClient {
|
|
|
299
317
|
* @returns Promise resolving to boolean indicating success
|
|
300
318
|
*/
|
|
301
319
|
sendTelemetry(eventLocation: string, eventCategory: string, eventAction: string, eventSubCategory?: string | null, eventSubAction?: string | null, eventDetails?: Record<string, any> | string | null, platformId?: string | null): Promise<boolean>;
|
|
320
|
+
/**
|
|
321
|
+
* Validates and enriches user-provided event details with device info,
|
|
322
|
+
* attribution data, and session context — matching the enrichment
|
|
323
|
+
* platform-v2 applies in sendAnalyticsEvent. Shared by the JWT and partner
|
|
324
|
+
* telemetry paths.
|
|
325
|
+
* @returns Enriched details object, or null if eventDetails is not valid JSON.
|
|
326
|
+
*/
|
|
327
|
+
private enrichEventDetails;
|
|
328
|
+
/**
|
|
329
|
+
* Sends a telemetry event via the partner analytics endpoint using a partner
|
|
330
|
+
* API key (x-api-key) instead of a hyve JWT. Used by externally-hosted games
|
|
331
|
+
* such as CrazyGames. game_id is derived from the API key server-side and is
|
|
332
|
+
* therefore not sent. hyve_user_id carries the `cg:`-prefixed CrazyGames id,
|
|
333
|
+
* or a guest sentinel when the player is not logged in.
|
|
334
|
+
*/
|
|
335
|
+
private sendPartnerTelemetry;
|
|
336
|
+
/**
|
|
337
|
+
* Seeds the CrazyGames user id used for telemetry attribution and registers
|
|
338
|
+
* an auth listener so the id updates if a guest logs in mid-session.
|
|
339
|
+
* The id is client-asserted (spoofable) and used only as an attribution label.
|
|
340
|
+
*/
|
|
341
|
+
private seedCrazyGamesUser;
|
|
302
342
|
/**
|
|
303
343
|
* Required lifecycle telemetry — Session start.
|
|
304
344
|
* See https://docs.hyve.gg/docs/telemetry#required-lifecycle-events
|
|
@@ -419,7 +459,15 @@ declare class HyveClient {
|
|
|
419
459
|
*/
|
|
420
460
|
reset(): void;
|
|
421
461
|
/**
|
|
422
|
-
* Get the storage adapter based on mode
|
|
462
|
+
* Get the storage adapter based on mode.
|
|
463
|
+
*
|
|
464
|
+
* Selection order:
|
|
465
|
+
* 1. An explicit `mode` override ('cloud' | 'local') always wins.
|
|
466
|
+
* 2. On the CrazyGames platform, the CrazyGames data store is auto-selected
|
|
467
|
+
* (D3 — public storageMode type is preserved; this is chosen internally).
|
|
468
|
+
* 3. Otherwise the configured storageMode is used.
|
|
469
|
+
*
|
|
470
|
+
* Awaits CrazyGames SDK initialization so the data store is ready before use.
|
|
423
471
|
* @param mode Storage mode override (cloud or local)
|
|
424
472
|
*/
|
|
425
473
|
private getStorageAdapter;
|
package/dist/react.js
CHANGED
|
@@ -1199,6 +1199,7 @@ var PlaygamaService = class {
|
|
|
1199
1199
|
var CRAZYGAMES_SDK_CDN = "https://sdk.crazygames.com/crazygames-sdk-v2.js";
|
|
1200
1200
|
var CrazyGamesService = class {
|
|
1201
1201
|
initialized = false;
|
|
1202
|
+
environment = null;
|
|
1202
1203
|
/**
|
|
1203
1204
|
* Detects if the game is running on the CrazyGames platform.
|
|
1204
1205
|
* Games on CrazyGames run inside an iframe, so we check document.referrer
|
|
@@ -1240,6 +1241,7 @@ var CrazyGamesService = class {
|
|
|
1240
1241
|
logger.warn("[CrazyGamesService] Unexpected environment:", env);
|
|
1241
1242
|
return false;
|
|
1242
1243
|
}
|
|
1244
|
+
this.environment = env;
|
|
1243
1245
|
this.initialized = true;
|
|
1244
1246
|
return true;
|
|
1245
1247
|
} catch (error) {
|
|
@@ -1350,6 +1352,88 @@ var CrazyGamesService = class {
|
|
|
1350
1352
|
if (!this.initialized) return;
|
|
1351
1353
|
window.CrazyGames?.SDK.game.happytime();
|
|
1352
1354
|
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Returns the resolved CrazyGames environment ('crazygames' | 'local' |
|
|
1357
|
+
* 'disabled'), or null if the SDK has not initialized yet. Used to
|
|
1358
|
+
* auto-select the CrazyGames storage adapter.
|
|
1359
|
+
*/
|
|
1360
|
+
getEnvironment() {
|
|
1361
|
+
return this.environment;
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Whether the CrazyGames account system is present (sync). When false,
|
|
1365
|
+
* there is no logged-in user concept and telemetry falls back to guests.
|
|
1366
|
+
*/
|
|
1367
|
+
isUserAccountAvailable() {
|
|
1368
|
+
if (!this.initialized) return false;
|
|
1369
|
+
return window.CrazyGames?.SDK.user.isUserAccountAvailable ?? false;
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Fetches the current CrazyGames user, or null if the player is not logged
|
|
1373
|
+
* in (guest). The returned `__dangerousUserId` is client-asserted and used
|
|
1374
|
+
* only as a telemetry attribution label.
|
|
1375
|
+
*/
|
|
1376
|
+
async getUser() {
|
|
1377
|
+
const sdk = window.CrazyGames?.SDK;
|
|
1378
|
+
if (!this.initialized || !sdk || !this.isUserAccountAvailable()) {
|
|
1379
|
+
return null;
|
|
1380
|
+
}
|
|
1381
|
+
try {
|
|
1382
|
+
return await sdk.user.getUser() ?? null;
|
|
1383
|
+
} catch (error) {
|
|
1384
|
+
logger.warn("[CrazyGamesService] getUser failed:", error);
|
|
1385
|
+
return null;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* Registers a listener that fires when a guest logs in mid-session.
|
|
1390
|
+
* No-op if the SDK or account system is unavailable.
|
|
1391
|
+
*/
|
|
1392
|
+
addAuthListener(callback) {
|
|
1393
|
+
const sdk = window.CrazyGames?.SDK;
|
|
1394
|
+
if (!this.initialized || !sdk || !this.isUserAccountAvailable()) return;
|
|
1395
|
+
try {
|
|
1396
|
+
sdk.user.addAuthListener(callback);
|
|
1397
|
+
} catch (error) {
|
|
1398
|
+
logger.warn("[CrazyGamesService] addAuthListener failed:", error);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
/** Removes a previously registered auth listener. */
|
|
1402
|
+
removeAuthListener(callback) {
|
|
1403
|
+
const sdk = window.CrazyGames?.SDK;
|
|
1404
|
+
if (!this.initialized || !sdk) return;
|
|
1405
|
+
try {
|
|
1406
|
+
sdk.user.removeAuthListener(callback);
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
logger.warn("[CrazyGamesService] removeAuthListener failed:", error);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Reads a value from the CrazyGames data store. Returns null when the SDK
|
|
1413
|
+
* is unavailable or the key is absent.
|
|
1414
|
+
*/
|
|
1415
|
+
async dataGetItem(key) {
|
|
1416
|
+
const sdk = window.CrazyGames?.SDK;
|
|
1417
|
+
if (!this.initialized || !sdk) return null;
|
|
1418
|
+
const value = await sdk.data.getItem(key);
|
|
1419
|
+
return value ?? null;
|
|
1420
|
+
}
|
|
1421
|
+
/** Writes a value to the CrazyGames data store. */
|
|
1422
|
+
async dataSetItem(key, value) {
|
|
1423
|
+
const sdk = window.CrazyGames?.SDK;
|
|
1424
|
+
if (!this.initialized || !sdk) {
|
|
1425
|
+
throw new Error("CrazyGames SDK not initialized");
|
|
1426
|
+
}
|
|
1427
|
+
await sdk.data.setItem(key, value);
|
|
1428
|
+
}
|
|
1429
|
+
/** Removes a value from the CrazyGames data store. */
|
|
1430
|
+
async dataRemoveItem(key) {
|
|
1431
|
+
const sdk = window.CrazyGames?.SDK;
|
|
1432
|
+
if (!this.initialized || !sdk) {
|
|
1433
|
+
throw new Error("CrazyGames SDK not initialized");
|
|
1434
|
+
}
|
|
1435
|
+
await sdk.data.removeItem(key);
|
|
1436
|
+
}
|
|
1353
1437
|
loadScript() {
|
|
1354
1438
|
return new Promise((resolve, reject) => {
|
|
1355
1439
|
if (window.CrazyGames?.SDK) {
|
|
@@ -2011,6 +2095,162 @@ var LocalStorageAdapter = class {
|
|
|
2011
2095
|
return count;
|
|
2012
2096
|
}
|
|
2013
2097
|
};
|
|
2098
|
+
function isGameDataObject(value) {
|
|
2099
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2100
|
+
}
|
|
2101
|
+
function toNumber(value) {
|
|
2102
|
+
return typeof value === "number" ? value : 0;
|
|
2103
|
+
}
|
|
2104
|
+
function applyOperation(operation, current, operand) {
|
|
2105
|
+
switch (operation) {
|
|
2106
|
+
case "set":
|
|
2107
|
+
return operand;
|
|
2108
|
+
case "add":
|
|
2109
|
+
return toNumber(current) + toNumber(operand);
|
|
2110
|
+
case "subtract":
|
|
2111
|
+
return toNumber(current) - toNumber(operand);
|
|
2112
|
+
case "multiply":
|
|
2113
|
+
return toNumber(current) * toNumber(operand);
|
|
2114
|
+
case "divide": {
|
|
2115
|
+
const divisor = toNumber(operand);
|
|
2116
|
+
if (divisor === 0) throw new Error("Cannot divide by zero");
|
|
2117
|
+
return toNumber(current) / divisor;
|
|
2118
|
+
}
|
|
2119
|
+
case "modulo": {
|
|
2120
|
+
const divisor = toNumber(operand);
|
|
2121
|
+
if (divisor === 0) throw new Error("Cannot modulo by zero");
|
|
2122
|
+
return toNumber(current) % divisor;
|
|
2123
|
+
}
|
|
2124
|
+
// For min/max, a missing current has nothing to compare against — adopt the operand.
|
|
2125
|
+
case "min":
|
|
2126
|
+
return typeof current === "number" ? Math.min(current, toNumber(operand)) : operand;
|
|
2127
|
+
case "max":
|
|
2128
|
+
return typeof current === "number" ? Math.max(current, toNumber(operand)) : operand;
|
|
2129
|
+
case "append": {
|
|
2130
|
+
const base = Array.isArray(current) ? [...current] : current === void 0 || current === null ? [] : [current];
|
|
2131
|
+
if (Array.isArray(operand)) {
|
|
2132
|
+
base.push(...operand);
|
|
2133
|
+
} else {
|
|
2134
|
+
base.push(operand);
|
|
2135
|
+
}
|
|
2136
|
+
return base;
|
|
2137
|
+
}
|
|
2138
|
+
default:
|
|
2139
|
+
return operand;
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
function getAtPath(value, path) {
|
|
2143
|
+
let current = value;
|
|
2144
|
+
for (const segment of path.split(".")) {
|
|
2145
|
+
if (!isGameDataObject(current)) return void 0;
|
|
2146
|
+
current = current[segment];
|
|
2147
|
+
}
|
|
2148
|
+
return current;
|
|
2149
|
+
}
|
|
2150
|
+
function setAtPath(value, path, newValue) {
|
|
2151
|
+
const segments = path.split(".");
|
|
2152
|
+
const root = isGameDataObject(value) ? { ...value } : {};
|
|
2153
|
+
let cursor = root;
|
|
2154
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
2155
|
+
const segment = segments[i];
|
|
2156
|
+
const existing = cursor[segment];
|
|
2157
|
+
const next = isGameDataObject(existing) ? { ...existing } : {};
|
|
2158
|
+
cursor[segment] = next;
|
|
2159
|
+
cursor = next;
|
|
2160
|
+
}
|
|
2161
|
+
cursor[segments[segments.length - 1]] = newValue;
|
|
2162
|
+
return root;
|
|
2163
|
+
}
|
|
2164
|
+
var CrazyGamesStorageAdapter = class {
|
|
2165
|
+
constructor(service) {
|
|
2166
|
+
this.service = service;
|
|
2167
|
+
}
|
|
2168
|
+
getStorageKey(gameId, key) {
|
|
2169
|
+
return `${gameId}:${key}`;
|
|
2170
|
+
}
|
|
2171
|
+
async saveGameData(gameId, key, value, operation, path) {
|
|
2172
|
+
const storageKey = this.getStorageKey(gameId, key);
|
|
2173
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2174
|
+
const existingRaw = await this.service.dataGetItem(storageKey);
|
|
2175
|
+
const existing = existingRaw ? JSON.parse(existingRaw) : null;
|
|
2176
|
+
const createdAt = existing?.created_at ?? now;
|
|
2177
|
+
const useOperation = operation !== void 0;
|
|
2178
|
+
let storedValue;
|
|
2179
|
+
let opResult;
|
|
2180
|
+
if (path) {
|
|
2181
|
+
const current = getAtPath(existing?.value, path);
|
|
2182
|
+
const next = useOperation ? applyOperation(operation, current, value) : value;
|
|
2183
|
+
storedValue = setAtPath(existing?.value, path, next);
|
|
2184
|
+
opResult = next;
|
|
2185
|
+
} else {
|
|
2186
|
+
const current = existing?.value;
|
|
2187
|
+
const next = useOperation ? applyOperation(operation, current, value) : value;
|
|
2188
|
+
storedValue = next;
|
|
2189
|
+
opResult = next;
|
|
2190
|
+
}
|
|
2191
|
+
const item = {
|
|
2192
|
+
key,
|
|
2193
|
+
value: storedValue,
|
|
2194
|
+
created_at: createdAt,
|
|
2195
|
+
updated_at: now
|
|
2196
|
+
};
|
|
2197
|
+
await this.service.dataSetItem(storageKey, JSON.stringify(item));
|
|
2198
|
+
const response = { success: true, message: "Data saved successfully" };
|
|
2199
|
+
if (useOperation && operation !== "set") {
|
|
2200
|
+
response.result = opResult;
|
|
2201
|
+
}
|
|
2202
|
+
return response;
|
|
2203
|
+
}
|
|
2204
|
+
async batchSaveGameData(gameId, items) {
|
|
2205
|
+
const results = [];
|
|
2206
|
+
let usedOperation = false;
|
|
2207
|
+
for (const item of items) {
|
|
2208
|
+
if (item.operation !== void 0) usedOperation = true;
|
|
2209
|
+
try {
|
|
2210
|
+
const saved = await this.saveGameData(gameId, item.key, item.value, item.operation, item.path);
|
|
2211
|
+
results.push({ key: item.key, success: true, ...saved.result !== void 0 && { result: saved.result } });
|
|
2212
|
+
} catch (error) {
|
|
2213
|
+
results.push({
|
|
2214
|
+
key: item.key,
|
|
2215
|
+
success: false,
|
|
2216
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
const allSucceeded = results.every((r) => r.success);
|
|
2221
|
+
return {
|
|
2222
|
+
success: allSucceeded,
|
|
2223
|
+
message: `${results.filter((r) => r.success).length}/${items.length} items saved`,
|
|
2224
|
+
...usedOperation && { results }
|
|
2225
|
+
};
|
|
2226
|
+
}
|
|
2227
|
+
async getGameData(gameId, key) {
|
|
2228
|
+
const raw = await this.service.dataGetItem(this.getStorageKey(gameId, key));
|
|
2229
|
+
return raw ? JSON.parse(raw) : null;
|
|
2230
|
+
}
|
|
2231
|
+
async getMultipleGameData(gameId, keys) {
|
|
2232
|
+
const items = [];
|
|
2233
|
+
for (const key of keys) {
|
|
2234
|
+
const item = await this.getGameData(gameId, key);
|
|
2235
|
+
if (item) items.push(item);
|
|
2236
|
+
}
|
|
2237
|
+
return items;
|
|
2238
|
+
}
|
|
2239
|
+
async deleteGameData(gameId, key) {
|
|
2240
|
+
const storageKey = this.getStorageKey(gameId, key);
|
|
2241
|
+
const existing = await this.service.dataGetItem(storageKey);
|
|
2242
|
+
if (existing === null) return false;
|
|
2243
|
+
await this.service.dataRemoveItem(storageKey);
|
|
2244
|
+
return true;
|
|
2245
|
+
}
|
|
2246
|
+
async deleteMultipleGameData(gameId, keys) {
|
|
2247
|
+
let count = 0;
|
|
2248
|
+
for (const key of keys) {
|
|
2249
|
+
if (await this.deleteGameData(gameId, key)) count++;
|
|
2250
|
+
}
|
|
2251
|
+
return count;
|
|
2252
|
+
}
|
|
2253
|
+
};
|
|
2014
2254
|
|
|
2015
2255
|
// src/core/client.ts
|
|
2016
2256
|
function determineEnvironmentFromParentUrl() {
|
|
@@ -2057,6 +2297,11 @@ var HyveClient = class {
|
|
|
2057
2297
|
storageMode;
|
|
2058
2298
|
cloudStorageAdapter;
|
|
2059
2299
|
localStorageAdapter;
|
|
2300
|
+
crazyGamesStorageAdapter = null;
|
|
2301
|
+
partnerApiKey = null;
|
|
2302
|
+
partnerApiBaseUrl = null;
|
|
2303
|
+
/** Raw (unprefixed) CrazyGames __dangerousUserId, seeded at init and on login. */
|
|
2304
|
+
crazyGamesUserId = null;
|
|
2060
2305
|
/**
|
|
2061
2306
|
* Creates a new HyveClient instance
|
|
2062
2307
|
* @param config Optional configuration including telemetry and ads
|
|
@@ -2084,10 +2329,16 @@ var HyveClient = class {
|
|
|
2084
2329
|
return success;
|
|
2085
2330
|
});
|
|
2086
2331
|
}
|
|
2332
|
+
this.partnerApiKey = config?.partnerApiKey ?? null;
|
|
2333
|
+
this.partnerApiBaseUrl = config?.partnerApiBaseUrl ?? null;
|
|
2087
2334
|
if (typeof window !== "undefined" && CrazyGamesService.isCrazyGamesDomain()) {
|
|
2088
2335
|
this.crazyGamesService = new CrazyGamesService();
|
|
2089
|
-
this.
|
|
2336
|
+
this.crazyGamesStorageAdapter = new CrazyGamesStorageAdapter(this.crazyGamesService);
|
|
2337
|
+
this.crazyGamesInitPromise = this.crazyGamesService.initialize().then(async (success) => {
|
|
2090
2338
|
logger.info("CrazyGames SDK initialized:", success);
|
|
2339
|
+
if (success) {
|
|
2340
|
+
await this.seedCrazyGamesUser();
|
|
2341
|
+
}
|
|
2091
2342
|
return success;
|
|
2092
2343
|
});
|
|
2093
2344
|
}
|
|
@@ -2183,6 +2434,16 @@ var HyveClient = class {
|
|
|
2183
2434
|
* @returns Promise resolving to boolean indicating success
|
|
2184
2435
|
*/
|
|
2185
2436
|
async sendTelemetry(eventLocation, eventCategory, eventAction, eventSubCategory, eventSubAction, eventDetails, platformId) {
|
|
2437
|
+
if (this.partnerApiKey) {
|
|
2438
|
+
return this.sendPartnerTelemetry(
|
|
2439
|
+
eventLocation,
|
|
2440
|
+
eventCategory,
|
|
2441
|
+
eventAction,
|
|
2442
|
+
eventSubCategory,
|
|
2443
|
+
eventSubAction,
|
|
2444
|
+
eventDetails
|
|
2445
|
+
);
|
|
2446
|
+
}
|
|
2186
2447
|
if (!this.jwtToken) {
|
|
2187
2448
|
logger.error("JWT token required. Ensure hyve-access and game-id are present in the URL.");
|
|
2188
2449
|
return false;
|
|
@@ -2192,42 +2453,11 @@ var HyveClient = class {
|
|
|
2192
2453
|
return false;
|
|
2193
2454
|
}
|
|
2194
2455
|
try {
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
JSON.parse(eventDetails);
|
|
2199
|
-
} else if (typeof eventDetails === "object") {
|
|
2200
|
-
JSON.stringify(eventDetails);
|
|
2201
|
-
}
|
|
2202
|
-
} catch (validationError) {
|
|
2203
|
-
logger.error("Invalid JSON in eventDetails:", validationError);
|
|
2204
|
-
logger.error("eventDetails value:", eventDetails);
|
|
2205
|
-
return false;
|
|
2206
|
-
}
|
|
2207
|
-
}
|
|
2208
|
-
let parsedEventDetails = {};
|
|
2209
|
-
if (eventDetails) {
|
|
2210
|
-
if (typeof eventDetails === "string") {
|
|
2211
|
-
try {
|
|
2212
|
-
parsedEventDetails = JSON.parse(eventDetails);
|
|
2213
|
-
} catch {
|
|
2214
|
-
}
|
|
2215
|
-
} else {
|
|
2216
|
-
parsedEventDetails = eventDetails;
|
|
2217
|
-
}
|
|
2456
|
+
const enrichedEventDetails = this.enrichEventDetails(eventDetails);
|
|
2457
|
+
if (enrichedEventDetails === null) {
|
|
2458
|
+
return false;
|
|
2218
2459
|
}
|
|
2219
2460
|
const attribution = getAttributionData();
|
|
2220
|
-
const enrichedEventDetails = {
|
|
2221
|
-
// Device info
|
|
2222
|
-
...getEssentialDeviceInfo(),
|
|
2223
|
-
// Attribution data
|
|
2224
|
-
...attribution,
|
|
2225
|
-
// Timestamp and session context
|
|
2226
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2227
|
-
session_id: this.sessionId,
|
|
2228
|
-
// Event-specific details can override any of the above
|
|
2229
|
-
...parsedEventDetails
|
|
2230
|
-
};
|
|
2231
2461
|
const telemetryEvent = {
|
|
2232
2462
|
game_id: this.gameId,
|
|
2233
2463
|
session_id: this.sessionId,
|
|
@@ -2266,6 +2496,122 @@ var HyveClient = class {
|
|
|
2266
2496
|
return false;
|
|
2267
2497
|
}
|
|
2268
2498
|
}
|
|
2499
|
+
/**
|
|
2500
|
+
* Validates and enriches user-provided event details with device info,
|
|
2501
|
+
* attribution data, and session context — matching the enrichment
|
|
2502
|
+
* platform-v2 applies in sendAnalyticsEvent. Shared by the JWT and partner
|
|
2503
|
+
* telemetry paths.
|
|
2504
|
+
* @returns Enriched details object, or null if eventDetails is not valid JSON.
|
|
2505
|
+
*/
|
|
2506
|
+
enrichEventDetails(eventDetails) {
|
|
2507
|
+
let parsedEventDetails = {};
|
|
2508
|
+
if (eventDetails) {
|
|
2509
|
+
try {
|
|
2510
|
+
if (typeof eventDetails === "string") {
|
|
2511
|
+
parsedEventDetails = JSON.parse(eventDetails);
|
|
2512
|
+
} else if (typeof eventDetails === "object") {
|
|
2513
|
+
JSON.stringify(eventDetails);
|
|
2514
|
+
parsedEventDetails = eventDetails;
|
|
2515
|
+
}
|
|
2516
|
+
} catch (validationError) {
|
|
2517
|
+
logger.error("Invalid JSON in eventDetails:", validationError);
|
|
2518
|
+
logger.error("eventDetails value:", eventDetails);
|
|
2519
|
+
return null;
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
const attribution = getAttributionData();
|
|
2523
|
+
return {
|
|
2524
|
+
// Device info
|
|
2525
|
+
...getEssentialDeviceInfo(),
|
|
2526
|
+
// Attribution data
|
|
2527
|
+
...attribution,
|
|
2528
|
+
// Timestamp and session context
|
|
2529
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2530
|
+
session_id: this.sessionId,
|
|
2531
|
+
// Event-specific details can override any of the above
|
|
2532
|
+
...parsedEventDetails
|
|
2533
|
+
};
|
|
2534
|
+
}
|
|
2535
|
+
/**
|
|
2536
|
+
* Sends a telemetry event via the partner analytics endpoint using a partner
|
|
2537
|
+
* API key (x-api-key) instead of a hyve JWT. Used by externally-hosted games
|
|
2538
|
+
* such as CrazyGames. game_id is derived from the API key server-side and is
|
|
2539
|
+
* therefore not sent. hyve_user_id carries the `cg:`-prefixed CrazyGames id,
|
|
2540
|
+
* or a guest sentinel when the player is not logged in.
|
|
2541
|
+
*/
|
|
2542
|
+
async sendPartnerTelemetry(eventLocation, eventCategory, eventAction, eventSubCategory, eventSubAction, eventDetails) {
|
|
2543
|
+
if (!this.partnerApiKey) {
|
|
2544
|
+
logger.error("Partner API key required for partner telemetry path.");
|
|
2545
|
+
return false;
|
|
2546
|
+
}
|
|
2547
|
+
if (this.crazyGamesService && this.crazyGamesInitPromise) {
|
|
2548
|
+
await this.crazyGamesInitPromise;
|
|
2549
|
+
}
|
|
2550
|
+
try {
|
|
2551
|
+
const enrichedEventDetails = this.enrichEventDetails(eventDetails);
|
|
2552
|
+
if (enrichedEventDetails === null) {
|
|
2553
|
+
return false;
|
|
2554
|
+
}
|
|
2555
|
+
const hyveUserId = this.crazyGamesUserId ? `cg:${this.crazyGamesUserId}` : "cg:guest";
|
|
2556
|
+
const partnerEvent = {
|
|
2557
|
+
session_id: this.sessionId,
|
|
2558
|
+
hyve_user_id: hyveUserId,
|
|
2559
|
+
event_location: eventLocation,
|
|
2560
|
+
event_category: eventCategory,
|
|
2561
|
+
event_sub_category: eventSubCategory || null,
|
|
2562
|
+
event_action: eventAction,
|
|
2563
|
+
event_sub_action: eventSubAction || null,
|
|
2564
|
+
event_details: enrichedEventDetails
|
|
2565
|
+
};
|
|
2566
|
+
logger.debug("Sending partner telemetry event:", partnerEvent);
|
|
2567
|
+
const base = this.partnerApiBaseUrl || this.apiBaseUrl;
|
|
2568
|
+
const telemetryUrl = `${base}/api/v1/partners/analytics/events`;
|
|
2569
|
+
const response = await fetch(telemetryUrl, {
|
|
2570
|
+
method: "POST",
|
|
2571
|
+
headers: {
|
|
2572
|
+
"Content-Type": "application/json",
|
|
2573
|
+
"x-api-key": this.partnerApiKey
|
|
2574
|
+
},
|
|
2575
|
+
body: JSON.stringify(partnerEvent)
|
|
2576
|
+
});
|
|
2577
|
+
if (response.ok) {
|
|
2578
|
+
logger.info("Partner telemetry event sent successfully:", response.status);
|
|
2579
|
+
return true;
|
|
2580
|
+
} else {
|
|
2581
|
+
const errorText = await response.text();
|
|
2582
|
+
logger.error(
|
|
2583
|
+
"Failed to send partner telemetry event:",
|
|
2584
|
+
response.status,
|
|
2585
|
+
errorText
|
|
2586
|
+
);
|
|
2587
|
+
return false;
|
|
2588
|
+
}
|
|
2589
|
+
} catch (error) {
|
|
2590
|
+
logger.error("Error sending partner telemetry event:", error);
|
|
2591
|
+
return false;
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
/**
|
|
2595
|
+
* Seeds the CrazyGames user id used for telemetry attribution and registers
|
|
2596
|
+
* an auth listener so the id updates if a guest logs in mid-session.
|
|
2597
|
+
* The id is client-asserted (spoofable) and used only as an attribution label.
|
|
2598
|
+
*/
|
|
2599
|
+
async seedCrazyGamesUser() {
|
|
2600
|
+
if (!this.crazyGamesService) return;
|
|
2601
|
+
if (!this.crazyGamesService.isUserAccountAvailable()) {
|
|
2602
|
+
logger.info("CrazyGames account system unavailable \u2014 telemetry attributed by session only");
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2605
|
+
const user = await this.crazyGamesService.getUser();
|
|
2606
|
+
this.crazyGamesUserId = user?.__dangerousUserId ?? null;
|
|
2607
|
+
if (this.crazyGamesUserId) {
|
|
2608
|
+
logger.info("CrazyGames user id seeded for telemetry attribution");
|
|
2609
|
+
}
|
|
2610
|
+
this.crazyGamesService.addAuthListener((updatedUser) => {
|
|
2611
|
+
this.crazyGamesUserId = updatedUser?.__dangerousUserId ?? null;
|
|
2612
|
+
logger.info("CrazyGames auth state changed; telemetry user id updated");
|
|
2613
|
+
});
|
|
2614
|
+
}
|
|
2269
2615
|
/**
|
|
2270
2616
|
* Required lifecycle telemetry — Session start.
|
|
2271
2617
|
* See https://docs.hyve.gg/docs/telemetry#required-lifecycle-events
|
|
@@ -2486,12 +2832,28 @@ var HyveClient = class {
|
|
|
2486
2832
|
logger.info("Client reset with new sessionId:", this.sessionId);
|
|
2487
2833
|
}
|
|
2488
2834
|
/**
|
|
2489
|
-
* Get the storage adapter based on mode
|
|
2835
|
+
* Get the storage adapter based on mode.
|
|
2836
|
+
*
|
|
2837
|
+
* Selection order:
|
|
2838
|
+
* 1. An explicit `mode` override ('cloud' | 'local') always wins.
|
|
2839
|
+
* 2. On the CrazyGames platform, the CrazyGames data store is auto-selected
|
|
2840
|
+
* (D3 — public storageMode type is preserved; this is chosen internally).
|
|
2841
|
+
* 3. Otherwise the configured storageMode is used.
|
|
2842
|
+
*
|
|
2843
|
+
* Awaits CrazyGames SDK initialization so the data store is ready before use.
|
|
2490
2844
|
* @param mode Storage mode override (cloud or local)
|
|
2491
2845
|
*/
|
|
2492
|
-
getStorageAdapter(mode) {
|
|
2493
|
-
|
|
2494
|
-
|
|
2846
|
+
async getStorageAdapter(mode) {
|
|
2847
|
+
if (mode) {
|
|
2848
|
+
return mode === "local" ? this.localStorageAdapter : this.cloudStorageAdapter;
|
|
2849
|
+
}
|
|
2850
|
+
if (this.crazyGamesService && this.crazyGamesStorageAdapter) {
|
|
2851
|
+
if (this.crazyGamesInitPromise) await this.crazyGamesInitPromise;
|
|
2852
|
+
if (this.crazyGamesService.getEnvironment() === "crazygames") {
|
|
2853
|
+
return this.crazyGamesStorageAdapter;
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
return this.storageMode === "local" ? this.localStorageAdapter : this.cloudStorageAdapter;
|
|
2495
2857
|
}
|
|
2496
2858
|
/**
|
|
2497
2859
|
* Returns the current game ID or throws if not available.
|
|
@@ -2515,7 +2877,7 @@ var HyveClient = class {
|
|
|
2515
2877
|
const gameId = this.requireGameId();
|
|
2516
2878
|
const storageMode = storage || this.storageMode;
|
|
2517
2879
|
logger.debug(`Saving game data to ${storageMode}: ${gameId}/${key}`);
|
|
2518
|
-
const adapter = this.getStorageAdapter(storage);
|
|
2880
|
+
const adapter = await this.getStorageAdapter(storage);
|
|
2519
2881
|
const response = await adapter.saveGameData(gameId, key, value, operation, path);
|
|
2520
2882
|
logger.info(`Game data saved successfully to ${storageMode}: ${gameId}/${key}`);
|
|
2521
2883
|
return response;
|
|
@@ -2530,7 +2892,7 @@ var HyveClient = class {
|
|
|
2530
2892
|
const gameId = this.requireGameId();
|
|
2531
2893
|
const storageMode = storage || this.storageMode;
|
|
2532
2894
|
logger.debug(`Batch saving ${items.length} game data entries to ${storageMode} for game: ${gameId}`);
|
|
2533
|
-
const adapter = this.getStorageAdapter(storage);
|
|
2895
|
+
const adapter = await this.getStorageAdapter(storage);
|
|
2534
2896
|
const response = await adapter.batchSaveGameData(gameId, items);
|
|
2535
2897
|
logger.info(`Batch saved ${items.length} game data entries successfully to ${storageMode}`);
|
|
2536
2898
|
return response;
|
|
@@ -2545,7 +2907,7 @@ var HyveClient = class {
|
|
|
2545
2907
|
const gameId = this.requireGameId();
|
|
2546
2908
|
const storageMode = storage || this.storageMode;
|
|
2547
2909
|
logger.debug(`Getting game data from ${storageMode}: ${gameId}/${key}`);
|
|
2548
|
-
const adapter = this.getStorageAdapter(storage);
|
|
2910
|
+
const adapter = await this.getStorageAdapter(storage);
|
|
2549
2911
|
const data = await adapter.getGameData(gameId, key);
|
|
2550
2912
|
if (data) {
|
|
2551
2913
|
logger.info(`Game data retrieved successfully from ${storageMode}: ${gameId}/${key}`);
|
|
@@ -2564,7 +2926,7 @@ var HyveClient = class {
|
|
|
2564
2926
|
const gameId = this.requireGameId();
|
|
2565
2927
|
const storageMode = storage || this.storageMode;
|
|
2566
2928
|
logger.debug(`Getting ${keys.length} game data entries from ${storageMode} for game: ${gameId}`);
|
|
2567
|
-
const adapter = this.getStorageAdapter(storage);
|
|
2929
|
+
const adapter = await this.getStorageAdapter(storage);
|
|
2568
2930
|
const data = await adapter.getMultipleGameData(gameId, keys);
|
|
2569
2931
|
logger.info(`Retrieved ${data.length} game data entries from ${storageMode}`);
|
|
2570
2932
|
return data;
|
|
@@ -2579,7 +2941,7 @@ var HyveClient = class {
|
|
|
2579
2941
|
const gameId = this.requireGameId();
|
|
2580
2942
|
const storageMode = storage || this.storageMode;
|
|
2581
2943
|
logger.debug(`Deleting game data from ${storageMode}: ${gameId}/${key}`);
|
|
2582
|
-
const adapter = this.getStorageAdapter(storage);
|
|
2944
|
+
const adapter = await this.getStorageAdapter(storage);
|
|
2583
2945
|
const deleted = await adapter.deleteGameData(gameId, key);
|
|
2584
2946
|
if (deleted) {
|
|
2585
2947
|
logger.info(`Game data deleted successfully from ${storageMode}: ${gameId}/${key}`);
|
|
@@ -2598,7 +2960,7 @@ var HyveClient = class {
|
|
|
2598
2960
|
const gameId = this.requireGameId();
|
|
2599
2961
|
const storageMode = storage || this.storageMode;
|
|
2600
2962
|
logger.debug(`Deleting ${keys.length} game data entries from ${storageMode} for game: ${gameId}`);
|
|
2601
|
-
const adapter = this.getStorageAdapter(storage);
|
|
2963
|
+
const adapter = await this.getStorageAdapter(storage);
|
|
2602
2964
|
const deletedCount = await adapter.deleteMultipleGameData(gameId, keys);
|
|
2603
2965
|
logger.info(`Deleted ${deletedCount} game data entries from ${storageMode}`);
|
|
2604
2966
|
return deletedCount;
|