@playcademy/sdk 0.3.1 → 0.3.3
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.ts +29 -30
- package/dist/index.js +343 -233
- package/dist/internal.d.ts +4356 -4027
- package/dist/internal.js +596 -477
- package/dist/server.d.ts +4 -11
- package/dist/server.js +50 -22
- package/dist/types.d.ts +3080 -3080
- package/package.json +10 -9
package/dist/internal.js
CHANGED
|
@@ -32,15 +32,15 @@ class PlaycademyMessaging {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
listen(type, handler) {
|
|
35
|
-
|
|
35
|
+
function postMessageListener(event) {
|
|
36
36
|
const messageEvent = event;
|
|
37
37
|
if (messageEvent.data?.type === type) {
|
|
38
38
|
handler(messageEvent.data.payload || messageEvent.data);
|
|
39
39
|
}
|
|
40
|
-
}
|
|
41
|
-
|
|
40
|
+
}
|
|
41
|
+
function customEventListener(event) {
|
|
42
42
|
handler(event.detail);
|
|
43
|
-
}
|
|
43
|
+
}
|
|
44
44
|
if (!this.listeners.has(type)) {
|
|
45
45
|
this.listeners.set(type, new Map);
|
|
46
46
|
}
|
|
@@ -50,7 +50,7 @@ class PlaycademyMessaging {
|
|
|
50
50
|
customEvent: customEventListener
|
|
51
51
|
});
|
|
52
52
|
window.addEventListener("message", postMessageListener);
|
|
53
|
-
|
|
53
|
+
globalThis.addEventListener(type, customEventListener);
|
|
54
54
|
}
|
|
55
55
|
unlisten(type, handler) {
|
|
56
56
|
const typeListeners = this.listeners.get(type);
|
|
@@ -59,14 +59,14 @@ class PlaycademyMessaging {
|
|
|
59
59
|
}
|
|
60
60
|
const listeners = typeListeners.get(handler);
|
|
61
61
|
window.removeEventListener("message", listeners.postMessage);
|
|
62
|
-
|
|
62
|
+
globalThis.removeEventListener(type, listeners.customEvent);
|
|
63
63
|
typeListeners.delete(handler);
|
|
64
64
|
if (typeListeners.size === 0) {
|
|
65
65
|
this.listeners.delete(type);
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
getMessagingContext(eventType) {
|
|
69
|
-
const isIframe = typeof window !== "undefined" &&
|
|
69
|
+
const isIframe = typeof globalThis.window !== "undefined" && globalThis.self !== window.top;
|
|
70
70
|
const iframeToParentEvents = [
|
|
71
71
|
"PLAYCADEMY_READY" /* READY */,
|
|
72
72
|
"PLAYCADEMY_EXIT" /* EXIT */,
|
|
@@ -89,18 +89,18 @@ class PlaycademyMessaging {
|
|
|
89
89
|
target.postMessage(messageData, origin);
|
|
90
90
|
}
|
|
91
91
|
sendViaCustomEvent(type, payload) {
|
|
92
|
-
|
|
92
|
+
globalThis.dispatchEvent(new CustomEvent(type, { detail: payload }));
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
var messaging = new PlaycademyMessaging;
|
|
96
96
|
|
|
97
97
|
// src/core/static/init.ts
|
|
98
98
|
async function getPlaycademyConfig(allowedParentOrigins) {
|
|
99
|
-
const preloaded =
|
|
99
|
+
const preloaded = globalThis.PLAYCADEMY;
|
|
100
100
|
if (preloaded?.token) {
|
|
101
101
|
return preloaded;
|
|
102
102
|
}
|
|
103
|
-
if (
|
|
103
|
+
if (globalThis.self !== window.top) {
|
|
104
104
|
return await waitForPlaycademyInit(allowedParentOrigins);
|
|
105
105
|
} else {
|
|
106
106
|
return createStandaloneConfig();
|
|
@@ -114,13 +114,14 @@ function getReferrerOrigin() {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
function buildAllowedOrigins(explicit) {
|
|
117
|
-
if (Array.isArray(explicit) && explicit.length > 0)
|
|
117
|
+
if (Array.isArray(explicit) && explicit.length > 0) {
|
|
118
118
|
return explicit;
|
|
119
|
+
}
|
|
119
120
|
const ref = getReferrerOrigin();
|
|
120
121
|
return ref ? [ref] : [];
|
|
121
122
|
}
|
|
122
123
|
function isOriginAllowed(origin, allowlist) {
|
|
123
|
-
if (
|
|
124
|
+
if (globalThis.location.hostname === "localhost" || globalThis.location.hostname === "127.0.0.1") {
|
|
124
125
|
return true;
|
|
125
126
|
}
|
|
126
127
|
if (!allowlist || allowlist.length === 0) {
|
|
@@ -136,14 +137,16 @@ async function waitForPlaycademyInit(allowedParentOrigins) {
|
|
|
136
137
|
const allowlist = buildAllowedOrigins(allowedParentOrigins);
|
|
137
138
|
let hasWarnedAboutUntrustedOrigin = false;
|
|
138
139
|
function warnAboutUntrustedOrigin(origin) {
|
|
139
|
-
if (hasWarnedAboutUntrustedOrigin)
|
|
140
|
+
if (hasWarnedAboutUntrustedOrigin) {
|
|
140
141
|
return;
|
|
142
|
+
}
|
|
141
143
|
hasWarnedAboutUntrustedOrigin = true;
|
|
142
144
|
console.warn("[Playcademy SDK] Ignoring INIT from untrusted origin:", origin);
|
|
143
145
|
}
|
|
144
|
-
|
|
145
|
-
if (event.data?.type !== "PLAYCADEMY_INIT" /* INIT */)
|
|
146
|
+
function handleMessage(event) {
|
|
147
|
+
if (event.data?.type !== "PLAYCADEMY_INIT" /* INIT */) {
|
|
146
148
|
return;
|
|
149
|
+
}
|
|
147
150
|
if (!isOriginAllowed(event.origin, allowlist)) {
|
|
148
151
|
warnAboutUntrustedOrigin(event.origin);
|
|
149
152
|
return;
|
|
@@ -151,9 +154,9 @@ async function waitForPlaycademyInit(allowedParentOrigins) {
|
|
|
151
154
|
contextReceived = true;
|
|
152
155
|
window.removeEventListener("message", handleMessage);
|
|
153
156
|
clearTimeout(timeoutId);
|
|
154
|
-
|
|
157
|
+
globalThis.PLAYCADEMY = event.data.payload;
|
|
155
158
|
resolve(event.data.payload);
|
|
156
|
-
}
|
|
159
|
+
}
|
|
157
160
|
window.addEventListener("message", handleMessage);
|
|
158
161
|
const timeoutId = setTimeout(() => {
|
|
159
162
|
if (!contextReceived) {
|
|
@@ -167,16 +170,16 @@ function createStandaloneConfig() {
|
|
|
167
170
|
console.debug("[Playcademy SDK] Standalone mode detected, creating mock context for sandbox development");
|
|
168
171
|
const mockConfig = {
|
|
169
172
|
baseUrl: "http://localhost:4321",
|
|
170
|
-
gameUrl:
|
|
173
|
+
gameUrl: globalThis.location.origin,
|
|
171
174
|
token: "mock-game-token-for-local-dev",
|
|
172
175
|
gameId: "mock-game-id-from-template",
|
|
173
176
|
realtimeUrl: undefined
|
|
174
177
|
};
|
|
175
|
-
|
|
178
|
+
globalThis.PLAYCADEMY = mockConfig;
|
|
176
179
|
return mockConfig;
|
|
177
180
|
}
|
|
178
181
|
async function init(options) {
|
|
179
|
-
if (typeof window === "undefined") {
|
|
182
|
+
if (typeof globalThis.window === "undefined") {
|
|
180
183
|
throw new Error("Playcademy SDK must run in a browser context");
|
|
181
184
|
}
|
|
182
185
|
const config = await getPlaycademyConfig(options?.allowedParentOrigins);
|
|
@@ -188,7 +191,7 @@ async function init(options) {
|
|
|
188
191
|
gameUrl: config.gameUrl,
|
|
189
192
|
token: config.token,
|
|
190
193
|
gameId: config.gameId,
|
|
191
|
-
autoStartSession:
|
|
194
|
+
autoStartSession: globalThis.self !== window.top,
|
|
192
195
|
onDisconnect: options?.onDisconnect,
|
|
193
196
|
enableConnectionMonitoring: options?.enableConnectionMonitoring
|
|
194
197
|
});
|
|
@@ -198,23 +201,23 @@ async function init(options) {
|
|
|
198
201
|
return client;
|
|
199
202
|
}
|
|
200
203
|
// ../logger/src/index.ts
|
|
201
|
-
|
|
204
|
+
function isBrowser() {
|
|
202
205
|
const g = globalThis;
|
|
203
206
|
return typeof g.window !== "undefined" && typeof g.document !== "undefined";
|
|
204
|
-
}
|
|
205
|
-
|
|
207
|
+
}
|
|
208
|
+
function isProduction() {
|
|
206
209
|
return typeof process !== "undefined" && false;
|
|
207
|
-
}
|
|
208
|
-
|
|
210
|
+
}
|
|
211
|
+
function isDevelopment() {
|
|
209
212
|
return typeof process !== "undefined" && true;
|
|
210
|
-
}
|
|
211
|
-
|
|
213
|
+
}
|
|
214
|
+
function isInteractiveTTY() {
|
|
212
215
|
return typeof process !== "undefined" && Boolean(process.stdout && process.stdout.isTTY);
|
|
213
|
-
}
|
|
214
|
-
|
|
216
|
+
}
|
|
217
|
+
function isSilent() {
|
|
215
218
|
return typeof process !== "undefined" && process.env.LOG_SILENT === "true";
|
|
216
|
-
}
|
|
217
|
-
|
|
219
|
+
}
|
|
220
|
+
function detectOutputFormat() {
|
|
218
221
|
if (isBrowser()) {
|
|
219
222
|
return "browser";
|
|
220
223
|
}
|
|
@@ -238,7 +241,7 @@ var detectOutputFormat = () => {
|
|
|
238
241
|
return "color-tty";
|
|
239
242
|
}
|
|
240
243
|
return "json-single-line";
|
|
241
|
-
}
|
|
244
|
+
}
|
|
242
245
|
var colors = {
|
|
243
246
|
reset: "\x1B[0m",
|
|
244
247
|
bold: "\x1B[1m",
|
|
@@ -249,21 +252,26 @@ var colors = {
|
|
|
249
252
|
cyan: "\x1B[36m",
|
|
250
253
|
gray: "\x1B[90m"
|
|
251
254
|
};
|
|
252
|
-
|
|
255
|
+
function getLevelColor(level) {
|
|
253
256
|
switch (level) {
|
|
254
|
-
case "debug":
|
|
257
|
+
case "debug": {
|
|
255
258
|
return colors.blue;
|
|
256
|
-
|
|
259
|
+
}
|
|
260
|
+
case "info": {
|
|
257
261
|
return colors.cyan;
|
|
258
|
-
|
|
262
|
+
}
|
|
263
|
+
case "warn": {
|
|
259
264
|
return colors.yellow;
|
|
260
|
-
|
|
265
|
+
}
|
|
266
|
+
case "error": {
|
|
261
267
|
return colors.red;
|
|
262
|
-
|
|
268
|
+
}
|
|
269
|
+
default: {
|
|
263
270
|
return colors.reset;
|
|
271
|
+
}
|
|
264
272
|
}
|
|
265
|
-
}
|
|
266
|
-
|
|
273
|
+
}
|
|
274
|
+
function formatBrowserOutput(level, message, context, scope) {
|
|
267
275
|
const timestamp = new Date().toISOString();
|
|
268
276
|
const levelUpper = level.toUpperCase();
|
|
269
277
|
const consoleMethod = getConsoleMethod(level);
|
|
@@ -273,8 +281,8 @@ var formatBrowserOutput = (level, message, context, scope) => {
|
|
|
273
281
|
} else {
|
|
274
282
|
consoleMethod(`[${timestamp}] ${levelUpper}`, `${scopePrefix}${message}`);
|
|
275
283
|
}
|
|
276
|
-
}
|
|
277
|
-
|
|
284
|
+
}
|
|
285
|
+
function formatColorTTY(level, message, context, scope) {
|
|
278
286
|
const timestamp = new Date().toISOString();
|
|
279
287
|
const levelColor = getLevelColor(level);
|
|
280
288
|
const levelUpper = level.toUpperCase().padEnd(5);
|
|
@@ -286,8 +294,8 @@ var formatColorTTY = (level, message, context, scope) => {
|
|
|
286
294
|
} else {
|
|
287
295
|
consoleMethod(`${coloredPrefix} ${scopePrefix}${message}`);
|
|
288
296
|
}
|
|
289
|
-
}
|
|
290
|
-
|
|
297
|
+
}
|
|
298
|
+
function formatJSONSingleLine(level, message, context, scope) {
|
|
291
299
|
const timestamp = new Date().toISOString();
|
|
292
300
|
const logEntry = {
|
|
293
301
|
timestamp,
|
|
@@ -298,8 +306,8 @@ var formatJSONSingleLine = (level, message, context, scope) => {
|
|
|
298
306
|
};
|
|
299
307
|
const consoleMethod = getConsoleMethod(level);
|
|
300
308
|
consoleMethod(JSON.stringify(logEntry));
|
|
301
|
-
}
|
|
302
|
-
|
|
309
|
+
}
|
|
310
|
+
function formatJSONPretty(level, message, context, scope) {
|
|
303
311
|
const timestamp = new Date().toISOString();
|
|
304
312
|
const logEntry = {
|
|
305
313
|
timestamp,
|
|
@@ -310,65 +318,76 @@ var formatJSONPretty = (level, message, context, scope) => {
|
|
|
310
318
|
};
|
|
311
319
|
const consoleMethod = getConsoleMethod(level);
|
|
312
320
|
consoleMethod(JSON.stringify(logEntry, null, 2));
|
|
313
|
-
}
|
|
314
|
-
|
|
321
|
+
}
|
|
322
|
+
function getConsoleMethod(level) {
|
|
315
323
|
switch (level) {
|
|
316
|
-
case "debug":
|
|
324
|
+
case "debug": {
|
|
317
325
|
return console.debug;
|
|
318
|
-
|
|
326
|
+
}
|
|
327
|
+
case "info": {
|
|
319
328
|
return console.info;
|
|
320
|
-
|
|
329
|
+
}
|
|
330
|
+
case "warn": {
|
|
321
331
|
return console.warn;
|
|
322
|
-
|
|
332
|
+
}
|
|
333
|
+
case "error": {
|
|
323
334
|
return console.error;
|
|
324
|
-
|
|
335
|
+
}
|
|
336
|
+
default: {
|
|
325
337
|
return console.log;
|
|
338
|
+
}
|
|
326
339
|
}
|
|
327
|
-
}
|
|
340
|
+
}
|
|
328
341
|
var levelPriority = {
|
|
329
342
|
debug: 0,
|
|
330
343
|
info: 1,
|
|
331
344
|
warn: 2,
|
|
332
345
|
error: 3
|
|
333
346
|
};
|
|
334
|
-
|
|
347
|
+
function getMinimumLogLevel() {
|
|
335
348
|
const envLevel = typeof process !== "undefined" ? (process.env.LOG_LEVEL ?? "").toLowerCase() : "";
|
|
336
349
|
if (envLevel && ["debug", "info", "warn", "error"].includes(envLevel)) {
|
|
337
350
|
return envLevel;
|
|
338
351
|
}
|
|
339
352
|
return isProduction() ? "info" : "debug";
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if (isSilent())
|
|
353
|
+
}
|
|
354
|
+
function shouldLog(level) {
|
|
355
|
+
if (isSilent()) {
|
|
343
356
|
return false;
|
|
357
|
+
}
|
|
344
358
|
const minLevel = getMinimumLogLevel();
|
|
345
359
|
return levelPriority[level] >= levelPriority[minLevel];
|
|
346
|
-
}
|
|
360
|
+
}
|
|
347
361
|
var customHandler;
|
|
348
|
-
|
|
349
|
-
if (!shouldLog(level))
|
|
362
|
+
function performLog(level, message, context, scope) {
|
|
363
|
+
if (!shouldLog(level)) {
|
|
350
364
|
return;
|
|
365
|
+
}
|
|
351
366
|
if (customHandler) {
|
|
352
367
|
customHandler(level, message, context, scope);
|
|
353
368
|
return;
|
|
354
369
|
}
|
|
355
370
|
const outputFormat = detectOutputFormat();
|
|
356
371
|
switch (outputFormat) {
|
|
357
|
-
case "browser":
|
|
372
|
+
case "browser": {
|
|
358
373
|
formatBrowserOutput(level, message, context, scope);
|
|
359
374
|
break;
|
|
360
|
-
|
|
375
|
+
}
|
|
376
|
+
case "color-tty": {
|
|
361
377
|
formatColorTTY(level, message, context, scope);
|
|
362
378
|
break;
|
|
363
|
-
|
|
379
|
+
}
|
|
380
|
+
case "json-single-line": {
|
|
364
381
|
formatJSONSingleLine(level, message, context, scope);
|
|
365
382
|
break;
|
|
366
|
-
|
|
383
|
+
}
|
|
384
|
+
case "json-pretty": {
|
|
367
385
|
formatJSONPretty(level, message, context, scope);
|
|
368
386
|
break;
|
|
387
|
+
}
|
|
369
388
|
}
|
|
370
|
-
}
|
|
371
|
-
|
|
389
|
+
}
|
|
390
|
+
function createLogger(scopeName) {
|
|
372
391
|
return {
|
|
373
392
|
debug: (message, context) => performLog("debug", message, context, scopeName),
|
|
374
393
|
info: (message, context) => performLog("info", message, context, scopeName),
|
|
@@ -377,7 +396,7 @@ var createLogger = (scopeName) => {
|
|
|
377
396
|
log: (level, message, context) => performLog(level, message, context, scopeName),
|
|
378
397
|
scope: (name) => createLogger(scopeName ? `${scopeName}.${name}` : name)
|
|
379
398
|
};
|
|
380
|
-
}
|
|
399
|
+
}
|
|
381
400
|
var log = createLogger();
|
|
382
401
|
|
|
383
402
|
// src/core/errors.ts
|
|
@@ -388,15 +407,25 @@ class PlaycademyError extends Error {
|
|
|
388
407
|
}
|
|
389
408
|
}
|
|
390
409
|
|
|
410
|
+
class ManifestError extends PlaycademyError {
|
|
411
|
+
kind;
|
|
412
|
+
constructor(message, kind) {
|
|
413
|
+
super(message);
|
|
414
|
+
this.name = "ManifestError";
|
|
415
|
+
this.kind = kind;
|
|
416
|
+
Object.setPrototypeOf(this, ManifestError.prototype);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
391
420
|
class ApiError extends Error {
|
|
392
|
-
status;
|
|
393
421
|
code;
|
|
394
422
|
details;
|
|
395
423
|
rawBody;
|
|
424
|
+
status;
|
|
396
425
|
constructor(status, code, message, details, rawBody) {
|
|
397
426
|
super(message);
|
|
398
|
-
this.status = status;
|
|
399
427
|
this.name = "ApiError";
|
|
428
|
+
this.status = status;
|
|
400
429
|
this.code = code;
|
|
401
430
|
this.details = details;
|
|
402
431
|
this.rawBody = rawBody;
|
|
@@ -427,38 +456,54 @@ class ApiError extends Error {
|
|
|
427
456
|
}
|
|
428
457
|
function statusCodeToErrorCode(status) {
|
|
429
458
|
switch (status) {
|
|
430
|
-
case 400:
|
|
459
|
+
case 400: {
|
|
431
460
|
return "BAD_REQUEST";
|
|
432
|
-
|
|
461
|
+
}
|
|
462
|
+
case 401: {
|
|
433
463
|
return "UNAUTHORIZED";
|
|
434
|
-
|
|
464
|
+
}
|
|
465
|
+
case 403: {
|
|
435
466
|
return "FORBIDDEN";
|
|
436
|
-
|
|
467
|
+
}
|
|
468
|
+
case 404: {
|
|
437
469
|
return "NOT_FOUND";
|
|
438
|
-
|
|
470
|
+
}
|
|
471
|
+
case 405: {
|
|
439
472
|
return "METHOD_NOT_ALLOWED";
|
|
440
|
-
|
|
473
|
+
}
|
|
474
|
+
case 409: {
|
|
441
475
|
return "CONFLICT";
|
|
442
|
-
|
|
476
|
+
}
|
|
477
|
+
case 410: {
|
|
443
478
|
return "GONE";
|
|
444
|
-
|
|
479
|
+
}
|
|
480
|
+
case 412: {
|
|
445
481
|
return "PRECONDITION_FAILED";
|
|
446
|
-
|
|
482
|
+
}
|
|
483
|
+
case 413: {
|
|
447
484
|
return "PAYLOAD_TOO_LARGE";
|
|
448
|
-
|
|
485
|
+
}
|
|
486
|
+
case 422: {
|
|
449
487
|
return "VALIDATION_FAILED";
|
|
450
|
-
|
|
488
|
+
}
|
|
489
|
+
case 429: {
|
|
451
490
|
return "TOO_MANY_REQUESTS";
|
|
452
|
-
|
|
491
|
+
}
|
|
492
|
+
case 500: {
|
|
453
493
|
return "INTERNAL_ERROR";
|
|
454
|
-
|
|
494
|
+
}
|
|
495
|
+
case 501: {
|
|
455
496
|
return "NOT_IMPLEMENTED";
|
|
456
|
-
|
|
497
|
+
}
|
|
498
|
+
case 503: {
|
|
457
499
|
return "SERVICE_UNAVAILABLE";
|
|
458
|
-
|
|
500
|
+
}
|
|
501
|
+
case 504: {
|
|
459
502
|
return "TIMEOUT";
|
|
460
|
-
|
|
503
|
+
}
|
|
504
|
+
default: {
|
|
461
505
|
return status >= 500 ? "INTERNAL_ERROR" : "BAD_REQUEST";
|
|
506
|
+
}
|
|
462
507
|
}
|
|
463
508
|
}
|
|
464
509
|
function extractApiErrorInfo(error) {
|
|
@@ -476,10 +521,10 @@ function extractApiErrorInfo(error) {
|
|
|
476
521
|
// src/core/static/login.ts
|
|
477
522
|
async function login(baseUrl, email, password) {
|
|
478
523
|
let url = baseUrl;
|
|
479
|
-
if (baseUrl.startsWith("/") && typeof window !== "undefined") {
|
|
480
|
-
url =
|
|
524
|
+
if (baseUrl.startsWith("/") && typeof globalThis.window !== "undefined") {
|
|
525
|
+
url = globalThis.location.origin + baseUrl;
|
|
481
526
|
}
|
|
482
|
-
url
|
|
527
|
+
url += "/auth/login";
|
|
483
528
|
const response = await fetch(url, {
|
|
484
529
|
method: "POST",
|
|
485
530
|
headers: {
|
|
@@ -490,7 +535,7 @@ async function login(baseUrl, email, password) {
|
|
|
490
535
|
if (!response.ok) {
|
|
491
536
|
try {
|
|
492
537
|
const errorData = await response.json();
|
|
493
|
-
const errorMessage = errorData && errorData.message ? String(errorData.message) : response.statusText;
|
|
538
|
+
const errorMessage = errorData && errorData.message !== undefined ? String(errorData.message) : response.statusText;
|
|
494
539
|
throw new PlaycademyError(errorMessage);
|
|
495
540
|
} catch (error) {
|
|
496
541
|
log.error("[Playcademy SDK] Failed to parse error response JSON, using status text instead:", { error });
|
|
@@ -504,7 +549,7 @@ async function generateSecureRandomString(length) {
|
|
|
504
549
|
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
505
550
|
const randomValues = new Uint8Array(length);
|
|
506
551
|
globalThis.crypto.getRandomValues(randomValues);
|
|
507
|
-
return
|
|
552
|
+
return [...randomValues].map((byte) => charset[byte % charset.length]).join("");
|
|
508
553
|
}
|
|
509
554
|
|
|
510
555
|
// src/core/auth/oauth.ts
|
|
@@ -546,8 +591,9 @@ function parseOAuthState(state) {
|
|
|
546
591
|
}
|
|
547
592
|
function getOAuthConfig(provider) {
|
|
548
593
|
const configGetter = OAUTH_CONFIGS[provider];
|
|
549
|
-
if (!configGetter)
|
|
594
|
+
if (!configGetter) {
|
|
550
595
|
throw new Error(`Unsupported auth provider: ${provider}`);
|
|
596
|
+
}
|
|
551
597
|
return configGetter();
|
|
552
598
|
}
|
|
553
599
|
|
|
@@ -574,11 +620,11 @@ function openPopupWindow(url, name = "auth-popup", width = 500, height = 600) {
|
|
|
574
620
|
return window.open(url, name, features);
|
|
575
621
|
}
|
|
576
622
|
function isInIframe() {
|
|
577
|
-
if (typeof window === "undefined") {
|
|
623
|
+
if (typeof globalThis.window === "undefined") {
|
|
578
624
|
return false;
|
|
579
625
|
}
|
|
580
626
|
try {
|
|
581
|
-
return
|
|
627
|
+
return globalThis.self !== window.top;
|
|
582
628
|
} catch {
|
|
583
629
|
return true;
|
|
584
630
|
}
|
|
@@ -631,9 +677,10 @@ async function initiatePopupFlow(options) {
|
|
|
631
677
|
async function waitForServerMessage(popup, onStateChange) {
|
|
632
678
|
return new Promise((resolve) => {
|
|
633
679
|
let resolved = false;
|
|
634
|
-
|
|
635
|
-
if (event.origin !==
|
|
680
|
+
function handleMessage(event) {
|
|
681
|
+
if (event.origin !== globalThis.location.origin) {
|
|
636
682
|
return;
|
|
683
|
+
}
|
|
637
684
|
const data = event.data;
|
|
638
685
|
if (data?.type === "PLAYCADEMY_AUTH_STATE_CHANGE") {
|
|
639
686
|
resolved = true;
|
|
@@ -660,7 +707,7 @@ async function waitForServerMessage(popup, onStateChange) {
|
|
|
660
707
|
});
|
|
661
708
|
}
|
|
662
709
|
}
|
|
663
|
-
}
|
|
710
|
+
}
|
|
664
711
|
window.addEventListener("message", handleMessage);
|
|
665
712
|
const checkClosed = setInterval(() => {
|
|
666
713
|
if (popup.closed && !resolved) {
|
|
@@ -722,7 +769,7 @@ async function initiateRedirectFlow(options) {
|
|
|
722
769
|
params.set("scope", config.scope);
|
|
723
770
|
}
|
|
724
771
|
const authUrl = `${config.authorizationEndpoint}?${params.toString()}`;
|
|
725
|
-
|
|
772
|
+
globalThis.location.href = authUrl;
|
|
726
773
|
return new Promise(() => {});
|
|
727
774
|
} catch (error) {
|
|
728
775
|
const errorMessage = error instanceof Error ? error.message : "Authentication failed";
|
|
@@ -738,14 +785,22 @@ async function initiateRedirectFlow(options) {
|
|
|
738
785
|
// src/core/auth/flows/unified.ts
|
|
739
786
|
async function initiateUnifiedFlow(options) {
|
|
740
787
|
const { mode = "auto" } = options;
|
|
741
|
-
|
|
788
|
+
let effectiveMode;
|
|
789
|
+
if (mode === "auto") {
|
|
790
|
+
effectiveMode = isInIframe() ? "popup" : "redirect";
|
|
791
|
+
} else {
|
|
792
|
+
effectiveMode = mode;
|
|
793
|
+
}
|
|
742
794
|
switch (effectiveMode) {
|
|
743
|
-
case "popup":
|
|
795
|
+
case "popup": {
|
|
744
796
|
return initiatePopupFlow(options);
|
|
745
|
-
|
|
797
|
+
}
|
|
798
|
+
case "redirect": {
|
|
746
799
|
return initiateRedirectFlow(options);
|
|
747
|
-
|
|
800
|
+
}
|
|
801
|
+
default: {
|
|
748
802
|
throw new Error(`Unsupported authentication mode: ${effectiveMode}`);
|
|
803
|
+
}
|
|
749
804
|
}
|
|
750
805
|
}
|
|
751
806
|
|
|
@@ -767,7 +822,7 @@ async function login2(client, options) {
|
|
|
767
822
|
provider: options.provider,
|
|
768
823
|
mode: options.mode || "auto",
|
|
769
824
|
callbackUrl: options.callbackUrl,
|
|
770
|
-
hasStateData:
|
|
825
|
+
hasStateData: Boolean(stateData)
|
|
771
826
|
});
|
|
772
827
|
const optionsWithState = {
|
|
773
828
|
...options,
|
|
@@ -802,13 +857,13 @@ function createIdentityNamespace(client) {
|
|
|
802
857
|
// src/namespaces/game/runtime.ts
|
|
803
858
|
function createRuntimeNamespace(client) {
|
|
804
859
|
const eventListeners = new Map;
|
|
805
|
-
|
|
860
|
+
function trackListener(eventType, handler) {
|
|
806
861
|
if (!eventListeners.has(eventType)) {
|
|
807
862
|
eventListeners.set(eventType, new Set);
|
|
808
863
|
}
|
|
809
864
|
eventListeners.get(eventType).add(handler);
|
|
810
|
-
}
|
|
811
|
-
|
|
865
|
+
}
|
|
866
|
+
function untrackListener(eventType, handler) {
|
|
812
867
|
const listeners = eventListeners.get(eventType);
|
|
813
868
|
if (listeners) {
|
|
814
869
|
listeners.delete(handler);
|
|
@@ -816,12 +871,9 @@ function createRuntimeNamespace(client) {
|
|
|
816
871
|
eventListeners.delete(eventType);
|
|
817
872
|
}
|
|
818
873
|
}
|
|
819
|
-
}
|
|
820
|
-
if (typeof window !== "undefined" &&
|
|
821
|
-
|
|
822
|
-
const forwardKeys = Array.isArray(playcademyConfig?.forwardKeys) ? playcademyConfig.forwardKeys : ["Escape"];
|
|
823
|
-
const keySet = new Set(forwardKeys.map((k) => k.toLowerCase()));
|
|
824
|
-
const keyListener = (event) => {
|
|
874
|
+
}
|
|
875
|
+
if (typeof globalThis.window !== "undefined" && globalThis.self !== window.top) {
|
|
876
|
+
let keyListener = function(event) {
|
|
825
877
|
if (keySet.has(event.key?.toLowerCase() ?? "") || keySet.has(event.code?.toLowerCase() ?? "")) {
|
|
826
878
|
messaging.send("PLAYCADEMY_KEY_EVENT" /* KEY_EVENT */, {
|
|
827
879
|
key: event.key,
|
|
@@ -830,11 +882,14 @@ function createRuntimeNamespace(client) {
|
|
|
830
882
|
});
|
|
831
883
|
}
|
|
832
884
|
};
|
|
833
|
-
|
|
834
|
-
|
|
885
|
+
const playcademyConfig = globalThis.PLAYCADEMY;
|
|
886
|
+
const forwardKeys = Array.isArray(playcademyConfig?.forwardKeys) ? playcademyConfig.forwardKeys : ["Escape"];
|
|
887
|
+
const keySet = new Set(forwardKeys.map((k) => k.toLowerCase()));
|
|
888
|
+
globalThis.addEventListener("keydown", keyListener);
|
|
889
|
+
globalThis.addEventListener("keyup", keyListener);
|
|
835
890
|
trackListener("PLAYCADEMY_FORCE_EXIT" /* FORCE_EXIT */, () => {
|
|
836
|
-
|
|
837
|
-
|
|
891
|
+
globalThis.removeEventListener("keydown", keyListener);
|
|
892
|
+
globalThis.removeEventListener("keyup", keyListener);
|
|
838
893
|
});
|
|
839
894
|
}
|
|
840
895
|
return {
|
|
@@ -911,32 +966,30 @@ function createRuntimeNamespace(client) {
|
|
|
911
966
|
};
|
|
912
967
|
}
|
|
913
968
|
function createAssetsNamespace(client) {
|
|
914
|
-
|
|
969
|
+
async function fetchAsset(path, options) {
|
|
915
970
|
const gameUrl = client["initPayload"]?.gameUrl;
|
|
916
971
|
if (!gameUrl) {
|
|
917
|
-
const relativePath = path.startsWith("./") ? path :
|
|
972
|
+
const relativePath = path.startsWith("./") ? path : `./${path}`;
|
|
918
973
|
return fetch(relativePath, options);
|
|
919
974
|
}
|
|
920
975
|
const cleanPath = path.startsWith("./") ? path.slice(2) : path;
|
|
921
|
-
return fetch(gameUrl
|
|
922
|
-
}
|
|
976
|
+
return fetch(`${gameUrl}${cleanPath}`, options);
|
|
977
|
+
}
|
|
923
978
|
return {
|
|
924
979
|
url(pathOrStrings, ...values) {
|
|
925
980
|
const gameUrl = client["initPayload"]?.gameUrl;
|
|
926
981
|
let path;
|
|
927
982
|
if (Array.isArray(pathOrStrings) && "raw" in pathOrStrings) {
|
|
928
983
|
const strings = pathOrStrings;
|
|
929
|
-
path = strings.reduce((acc, str, i) =>
|
|
930
|
-
return acc + str + (values[i] != null ? String(values[i]) : "");
|
|
931
|
-
}, "");
|
|
984
|
+
path = strings.reduce((acc, str, i) => acc + str + (values[i] != null ? String(values[i]) : ""), "");
|
|
932
985
|
} else {
|
|
933
986
|
path = pathOrStrings;
|
|
934
987
|
}
|
|
935
988
|
if (!gameUrl) {
|
|
936
|
-
return path.startsWith("./") ? path :
|
|
989
|
+
return path.startsWith("./") ? path : `./${path}`;
|
|
937
990
|
}
|
|
938
991
|
const cleanPath = path.startsWith("./") ? path.slice(2) : path;
|
|
939
|
-
return gameUrl
|
|
992
|
+
return `${gameUrl}${cleanPath}`;
|
|
940
993
|
},
|
|
941
994
|
fetch: fetchAsset,
|
|
942
995
|
json: async (path) => {
|
|
@@ -958,10 +1011,10 @@ function createAssetsNamespace(client) {
|
|
|
958
1011
|
};
|
|
959
1012
|
}
|
|
960
1013
|
// src/namespaces/game/backend.ts
|
|
1014
|
+
function normalizePath(path) {
|
|
1015
|
+
return path.startsWith("/") ? path : `/${path}`;
|
|
1016
|
+
}
|
|
961
1017
|
function createBackendNamespace(client) {
|
|
962
|
-
function normalizePath(path) {
|
|
963
|
-
return path.startsWith("/") ? path : `/${path}`;
|
|
964
|
-
}
|
|
965
1018
|
return {
|
|
966
1019
|
async get(path, headers) {
|
|
967
1020
|
return client["requestGameBackend"](normalizePath(path), "GET", undefined, headers);
|
|
@@ -987,9 +1040,7 @@ function createBackendNamespace(client) {
|
|
|
987
1040
|
url(pathOrStrings, ...values) {
|
|
988
1041
|
if (Array.isArray(pathOrStrings) && "raw" in pathOrStrings) {
|
|
989
1042
|
const strings = pathOrStrings;
|
|
990
|
-
const path2 = strings.reduce((acc, str, i) =>
|
|
991
|
-
return acc + str + (values[i] != null ? String(values[i]) : "");
|
|
992
|
-
}, "");
|
|
1043
|
+
const path2 = strings.reduce((acc, str, i) => acc + str + (values[i] != null ? String(values[i]) : ""), "");
|
|
993
1044
|
return `${client.gameUrl}/api${path2.startsWith("/") ? path2 : `/${path2}`}`;
|
|
994
1045
|
}
|
|
995
1046
|
const path = pathOrStrings;
|
|
@@ -1003,8 +1054,9 @@ function createPermanentCache(keyPrefix) {
|
|
|
1003
1054
|
async function get(key, loader) {
|
|
1004
1055
|
const fullKey = keyPrefix ? `${keyPrefix}:${key}` : key;
|
|
1005
1056
|
const existing = cache.get(fullKey);
|
|
1006
|
-
if (existing)
|
|
1057
|
+
if (existing) {
|
|
1007
1058
|
return existing;
|
|
1059
|
+
}
|
|
1008
1060
|
const promise = loader().catch((error) => {
|
|
1009
1061
|
cache.delete(fullKey);
|
|
1010
1062
|
throw error;
|
|
@@ -1042,23 +1094,23 @@ function createPermanentCache(keyPrefix) {
|
|
|
1042
1094
|
function createUsersNamespace(client) {
|
|
1043
1095
|
const itemIdCache = createPermanentCache("items");
|
|
1044
1096
|
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1045
|
-
|
|
1046
|
-
if (UUID_REGEX.test(identifier))
|
|
1097
|
+
async function resolveItemId(identifier) {
|
|
1098
|
+
if (UUID_REGEX.test(identifier)) {
|
|
1047
1099
|
return identifier;
|
|
1100
|
+
}
|
|
1048
1101
|
const gameId = client["gameId"];
|
|
1049
1102
|
const cacheKey = gameId ? `${identifier}:${gameId}` : identifier;
|
|
1050
1103
|
return itemIdCache.get(cacheKey, async () => {
|
|
1051
1104
|
const queryParams = new URLSearchParams({ slug: identifier });
|
|
1052
|
-
if (gameId)
|
|
1105
|
+
if (gameId) {
|
|
1053
1106
|
queryParams.append("gameId", gameId);
|
|
1107
|
+
}
|
|
1054
1108
|
const item = await client["request"](`/items/resolve?${queryParams.toString()}`, "GET");
|
|
1055
1109
|
return item.id;
|
|
1056
1110
|
});
|
|
1057
|
-
}
|
|
1111
|
+
}
|
|
1058
1112
|
return {
|
|
1059
|
-
me: async () =>
|
|
1060
|
-
return client["request"]("/users/me", "GET");
|
|
1061
|
-
},
|
|
1113
|
+
me: async () => client["request"]("/users/me", "GET"),
|
|
1062
1114
|
inventory: {
|
|
1063
1115
|
get: async () => client["request"](`/inventory`, "GET"),
|
|
1064
1116
|
add: async (identifier, qty) => {
|
|
@@ -1185,8 +1237,15 @@ var ACHIEVEMENT_DEFINITIONS = [
|
|
|
1185
1237
|
}
|
|
1186
1238
|
];
|
|
1187
1239
|
// ../constants/src/typescript.ts
|
|
1188
|
-
var
|
|
1189
|
-
|
|
1240
|
+
var TypeScriptPackages = {
|
|
1241
|
+
tsc: "tsc",
|
|
1242
|
+
nativePreview: "@typescript/native-preview",
|
|
1243
|
+
nativePreviewPinned: "@typescript/native-preview@7.0.0-dev.20260221.1"
|
|
1244
|
+
};
|
|
1245
|
+
var TYPESCRIPT_RUNNER = {
|
|
1246
|
+
package: TypeScriptPackages.nativePreviewPinned,
|
|
1247
|
+
bin: "tsgo"
|
|
1248
|
+
};
|
|
1190
1249
|
// ../constants/src/overworld.ts
|
|
1191
1250
|
var ITEM_SLUGS = {
|
|
1192
1251
|
PLAYCADEMY_CREDITS: "PLAYCADEMY_CREDITS",
|
|
@@ -1241,7 +1300,7 @@ function createSingletonCache() {
|
|
|
1241
1300
|
// src/namespaces/game/credits.ts
|
|
1242
1301
|
function createCreditsNamespace(client) {
|
|
1243
1302
|
const creditsIdCache = createSingletonCache();
|
|
1244
|
-
|
|
1303
|
+
async function getCreditsItemId() {
|
|
1245
1304
|
return creditsIdCache.get(async () => {
|
|
1246
1305
|
const queryParams = new URLSearchParams({ slug: CURRENCIES.PRIMARY });
|
|
1247
1306
|
const creditsItem = await client["request"](`/items/resolve?${queryParams.toString()}`, "GET");
|
|
@@ -1250,7 +1309,7 @@ function createCreditsNamespace(client) {
|
|
|
1250
1309
|
}
|
|
1251
1310
|
return creditsItem.id;
|
|
1252
1311
|
});
|
|
1253
|
-
}
|
|
1312
|
+
}
|
|
1254
1313
|
return {
|
|
1255
1314
|
balance: async () => {
|
|
1256
1315
|
const inventory = await client["request"]("/inventory", "GET");
|
|
@@ -1298,14 +1357,12 @@ function createCreditsNamespace(client) {
|
|
|
1298
1357
|
// src/namespaces/game/scores.ts
|
|
1299
1358
|
function createScoresNamespace(client) {
|
|
1300
1359
|
return {
|
|
1301
|
-
submit: async (gameId, score, metadata) => {
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
});
|
|
1308
|
-
}
|
|
1360
|
+
submit: async (gameId, score, metadata) => client["request"](`/games/${gameId}/scores`, "POST", {
|
|
1361
|
+
body: {
|
|
1362
|
+
score,
|
|
1363
|
+
metadata
|
|
1364
|
+
}
|
|
1365
|
+
})
|
|
1309
1366
|
};
|
|
1310
1367
|
}
|
|
1311
1368
|
// src/namespaces/game/realtime.ts
|
|
@@ -1379,8 +1436,9 @@ function createTTLCache(options) {
|
|
|
1379
1436
|
function has(key) {
|
|
1380
1437
|
const fullKey = keyPrefix ? `${keyPrefix}:${key}` : key;
|
|
1381
1438
|
const cached = cache.get(fullKey);
|
|
1382
|
-
if (!cached)
|
|
1439
|
+
if (!cached) {
|
|
1383
1440
|
return false;
|
|
1441
|
+
}
|
|
1384
1442
|
const now = Date.now();
|
|
1385
1443
|
if (cached.expiresAt <= now) {
|
|
1386
1444
|
cache.delete(fullKey);
|
|
@@ -1422,7 +1480,9 @@ function createTimebackNamespace(client) {
|
|
|
1422
1480
|
ttl: 5000,
|
|
1423
1481
|
keyPrefix: "game.timeback.xp"
|
|
1424
1482
|
});
|
|
1425
|
-
|
|
1483
|
+
function getTimeback() {
|
|
1484
|
+
return client["initPayload"]?.timeback;
|
|
1485
|
+
}
|
|
1426
1486
|
return {
|
|
1427
1487
|
get user() {
|
|
1428
1488
|
return {
|
|
@@ -1438,21 +1498,19 @@ function createTimebackNamespace(client) {
|
|
|
1438
1498
|
get organizations() {
|
|
1439
1499
|
return getTimeback()?.organizations ?? [];
|
|
1440
1500
|
},
|
|
1441
|
-
fetch: async (options) => {
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
}, options);
|
|
1455
|
-
},
|
|
1501
|
+
fetch: async (options) => userCache.get("current", async () => {
|
|
1502
|
+
const response = await client["request"]("/timeback/user", "GET");
|
|
1503
|
+
const initPayload = client["initPayload"];
|
|
1504
|
+
if (initPayload) {
|
|
1505
|
+
initPayload.timeback = response;
|
|
1506
|
+
}
|
|
1507
|
+
return {
|
|
1508
|
+
id: response.id,
|
|
1509
|
+
role: response.role,
|
|
1510
|
+
enrollments: response.enrollments,
|
|
1511
|
+
organizations: response.organizations
|
|
1512
|
+
};
|
|
1513
|
+
}, options),
|
|
1456
1514
|
xp: {
|
|
1457
1515
|
fetch: async (options) => {
|
|
1458
1516
|
const hasGrade = options?.grade !== undefined;
|
|
@@ -1590,22 +1648,18 @@ function createAuthNamespace(client) {
|
|
|
1590
1648
|
client.setToken(null);
|
|
1591
1649
|
},
|
|
1592
1650
|
apiKeys: {
|
|
1593
|
-
create: async (options) => {
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
dev: ["read", "write"]
|
|
1602
|
-
}
|
|
1651
|
+
create: async (options) => client["request"]("/dev/api-keys", "POST", {
|
|
1652
|
+
body: {
|
|
1653
|
+
name: options?.name || `SDK Key - ${new Date().toISOString()}`,
|
|
1654
|
+
expiresIn: options?.expiresIn !== undefined ? options.expiresIn : null,
|
|
1655
|
+
permissions: options?.permissions || {
|
|
1656
|
+
games: ["read", "write", "delete"],
|
|
1657
|
+
users: ["read:self", "write:self"],
|
|
1658
|
+
dev: ["read", "write"]
|
|
1603
1659
|
}
|
|
1604
|
-
}
|
|
1605
|
-
},
|
|
1606
|
-
list: async () =>
|
|
1607
|
-
return client["request"]("/auth/api-key/list", "GET");
|
|
1608
|
-
},
|
|
1660
|
+
}
|
|
1661
|
+
}),
|
|
1662
|
+
list: async () => client["request"]("/auth/api-key/list", "GET"),
|
|
1609
1663
|
revoke: async (keyId) => {
|
|
1610
1664
|
await client["request"]("/auth/api-key/revoke", "POST", {
|
|
1611
1665
|
body: { id: keyId }
|
|
@@ -1648,19 +1702,139 @@ function createAdminNamespace(client) {
|
|
|
1648
1702
|
}
|
|
1649
1703
|
};
|
|
1650
1704
|
}
|
|
1651
|
-
// src/
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1705
|
+
// src/core/deploy.ts
|
|
1706
|
+
class DeployPipeline {
|
|
1707
|
+
static POLL_INTERVAL_MS = 1000;
|
|
1708
|
+
static INACTIVITY_TIMEOUT_MS = 60 * 1000;
|
|
1709
|
+
static GAME_FETCH_RETRIES = 3;
|
|
1710
|
+
static MAX_INLINE_REQUEST_BYTES = 5.5 * 1024 * 1024;
|
|
1711
|
+
static textEncoder = new TextEncoder;
|
|
1712
|
+
client;
|
|
1713
|
+
constructor(client) {
|
|
1714
|
+
this.client = client;
|
|
1715
|
+
}
|
|
1716
|
+
async uploadFile(file, gameId, hooks) {
|
|
1717
|
+
const fileName = file instanceof File ? file.name : "game.zip";
|
|
1718
|
+
const { presignedUrl, uploadToken } = await this.initiateUpload(fileName, gameId);
|
|
1719
|
+
const contentType = file.type || "application/octet-stream";
|
|
1720
|
+
if (hooks?.onEvent && typeof XMLHttpRequest !== "undefined") {
|
|
1721
|
+
await this.uploadViaXHR(presignedUrl, file, contentType, hooks);
|
|
1722
|
+
} else {
|
|
1723
|
+
await this.uploadViaFetch(presignedUrl, file, contentType);
|
|
1724
|
+
}
|
|
1725
|
+
return uploadToken;
|
|
1726
|
+
}
|
|
1727
|
+
async submit(args) {
|
|
1728
|
+
const { requestBody } = await this.buildRequestBody(args);
|
|
1729
|
+
const { slug, hooks } = args;
|
|
1730
|
+
const job = await this.client["request"](`/games/${slug}/deploy`, "POST", { body: requestBody });
|
|
1731
|
+
const completedJob = await this.poll(slug, job.id, hooks);
|
|
1732
|
+
if (!completedJob.result?.url) {
|
|
1733
|
+
throw new Error("Deployment completed but no deployment URL was recorded");
|
|
1734
|
+
}
|
|
1735
|
+
return this.fetchGameWithRetry(slug);
|
|
1736
|
+
}
|
|
1737
|
+
async buildRequestBody(args) {
|
|
1738
|
+
const game = await this.resolveGame(args.slug, args.game);
|
|
1739
|
+
const requestBody = {};
|
|
1740
|
+
if (args.uploadToken) {
|
|
1741
|
+
requestBody.uploadToken = args.uploadToken;
|
|
1742
|
+
}
|
|
1743
|
+
if (args.metadata) {
|
|
1744
|
+
requestBody.metadata = args.metadata;
|
|
1745
|
+
}
|
|
1746
|
+
if (!args.backend) {
|
|
1747
|
+
return { game, requestBody };
|
|
1748
|
+
}
|
|
1749
|
+
const backendFields = {
|
|
1750
|
+
config: args.backend.config,
|
|
1751
|
+
...args.backend.bindings ? { bindings: args.backend.bindings } : {},
|
|
1752
|
+
...args.backend.schema ? { schema: args.backend.schema } : {}
|
|
1753
|
+
};
|
|
1754
|
+
const inlineBody = {
|
|
1755
|
+
...requestBody,
|
|
1756
|
+
...backendFields,
|
|
1757
|
+
code: args.backend.code
|
|
1758
|
+
};
|
|
1759
|
+
if (this.serializedSize(inlineBody) <= DeployPipeline.MAX_INLINE_REQUEST_BYTES) {
|
|
1760
|
+
return { game, requestBody: inlineBody };
|
|
1761
|
+
}
|
|
1762
|
+
const skeletonBody = {
|
|
1763
|
+
...requestBody,
|
|
1764
|
+
...backendFields,
|
|
1765
|
+
codeUploadToken: "________placeholder________"
|
|
1766
|
+
};
|
|
1767
|
+
if (this.serializedSize(skeletonBody) > DeployPipeline.MAX_INLINE_REQUEST_BYTES) {
|
|
1768
|
+
throw new Error("Deploy request is too large even after uploading backend code");
|
|
1769
|
+
}
|
|
1770
|
+
skeletonBody.codeUploadToken = await this.uploadCode(game.id, args.backend.code);
|
|
1771
|
+
return { game, requestBody: skeletonBody };
|
|
1772
|
+
}
|
|
1773
|
+
serializedSize(body) {
|
|
1774
|
+
return DeployPipeline.textEncoder.encode(JSON.stringify(body)).length;
|
|
1775
|
+
}
|
|
1776
|
+
async initiateUpload(fileName, gameId) {
|
|
1777
|
+
return this.client["request"]("/games/uploads/initiate/", "POST", { body: { fileName, gameId } });
|
|
1778
|
+
}
|
|
1779
|
+
async uploadCode(gameId, code) {
|
|
1780
|
+
const { presignedUrl, uploadToken } = await this.initiateUpload("backend.js", gameId);
|
|
1781
|
+
const res = await fetch(presignedUrl, {
|
|
1782
|
+
method: "PUT",
|
|
1783
|
+
body: code,
|
|
1784
|
+
headers: { "Content-Type": "application/javascript" }
|
|
1785
|
+
});
|
|
1786
|
+
if (!res.ok) {
|
|
1787
|
+
throw new Error(`Backend code upload failed: ${res.status} ${res.statusText}`);
|
|
1788
|
+
}
|
|
1789
|
+
return uploadToken;
|
|
1790
|
+
}
|
|
1791
|
+
async uploadViaFetch(url, body, contentType) {
|
|
1792
|
+
const res = await fetch(url, {
|
|
1793
|
+
method: "PUT",
|
|
1794
|
+
body,
|
|
1795
|
+
headers: { "Content-Type": contentType }
|
|
1796
|
+
});
|
|
1797
|
+
if (!res.ok) {
|
|
1798
|
+
throw new Error(`File upload failed: ${res.status} ${res.statusText}`);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
uploadViaXHR(url, body, contentType, hooks) {
|
|
1802
|
+
return new Promise((resolve, reject) => {
|
|
1803
|
+
const xhr = new XMLHttpRequest;
|
|
1804
|
+
xhr.open("PUT", url, true);
|
|
1805
|
+
try {
|
|
1806
|
+
xhr.setRequestHeader("Content-Type", contentType);
|
|
1807
|
+
} catch {}
|
|
1808
|
+
xhr.upload.addEventListener("progress", (event) => {
|
|
1809
|
+
if (event.lengthComputable) {
|
|
1810
|
+
hooks.onEvent?.({
|
|
1811
|
+
type: "s3Progress",
|
|
1812
|
+
loaded: event.loaded,
|
|
1813
|
+
total: event.total,
|
|
1814
|
+
percent: event.loaded / event.total
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
});
|
|
1818
|
+
xhr.addEventListener("load", () => {
|
|
1819
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
1820
|
+
resolve();
|
|
1821
|
+
} else {
|
|
1822
|
+
reject(new Error(`File upload failed: ${xhr.status} ${xhr.statusText}`));
|
|
1823
|
+
}
|
|
1824
|
+
});
|
|
1825
|
+
xhr.addEventListener("error", () => reject(new Error("File upload failed: network error")));
|
|
1826
|
+
xhr.send(body);
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
async poll(slug, jobId, hooks) {
|
|
1656
1830
|
hooks?.onEvent?.({ type: "finalizeStart" });
|
|
1657
1831
|
let seenEvents = 0;
|
|
1658
1832
|
let lastProgressAt = Date.now();
|
|
1659
1833
|
while (true) {
|
|
1660
|
-
if (Date.now() - lastProgressAt >
|
|
1834
|
+
if (Date.now() - lastProgressAt > DeployPipeline.INACTIVITY_TIMEOUT_MS) {
|
|
1661
1835
|
throw new Error("Deployment job timed out after 1 minute without progress");
|
|
1662
1836
|
}
|
|
1663
|
-
const job = await client["request"](`/games/${slug}/deploy?jobId=${encodeURIComponent(jobId)}`, "GET");
|
|
1837
|
+
const job = await this.client["request"](`/games/${slug}/deploy?jobId=${encodeURIComponent(jobId)}`, "GET");
|
|
1664
1838
|
const newEvents = job.events.slice(seenEvents);
|
|
1665
1839
|
seenEvents = job.events.length;
|
|
1666
1840
|
if (newEvents.length > 0) {
|
|
@@ -1679,9 +1853,30 @@ function createDevNamespace(client) {
|
|
|
1679
1853
|
if (job.status === "failed") {
|
|
1680
1854
|
throw new ApiError(job.errorStatus ?? 500, job.errorCode ?? "INTERNAL_ERROR", job.error || "Deployment failed", job);
|
|
1681
1855
|
}
|
|
1682
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
1856
|
+
await new Promise((resolve) => setTimeout(resolve, DeployPipeline.POLL_INTERVAL_MS));
|
|
1683
1857
|
}
|
|
1684
1858
|
}
|
|
1859
|
+
async resolveGame(slug, game) {
|
|
1860
|
+
return game ?? await this.client["request"](`/games/${slug}`, "GET");
|
|
1861
|
+
}
|
|
1862
|
+
async fetchGameWithRetry(slug) {
|
|
1863
|
+
const { GAME_FETCH_RETRIES } = DeployPipeline;
|
|
1864
|
+
for (let attempt = 0;attempt < GAME_FETCH_RETRIES; attempt++) {
|
|
1865
|
+
try {
|
|
1866
|
+
return await this.client["request"](`/games/${slug}`, "GET");
|
|
1867
|
+
} catch {
|
|
1868
|
+
if (attempt < GAME_FETCH_RETRIES - 1) {
|
|
1869
|
+
await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
throw new Error(`Deploy succeeded but failed to fetch updated game after ${GAME_FETCH_RETRIES} attempts`);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
// src/namespaces/platform/dev.ts
|
|
1878
|
+
function createDevNamespace(client) {
|
|
1879
|
+
const deploy = new DeployPipeline(client);
|
|
1685
1880
|
return {
|
|
1686
1881
|
status: {
|
|
1687
1882
|
apply: () => client["request"]("/dev/apply", "POST"),
|
|
@@ -1703,99 +1898,18 @@ function createDevNamespace(client) {
|
|
|
1703
1898
|
return game;
|
|
1704
1899
|
}
|
|
1705
1900
|
}
|
|
1706
|
-
|
|
1707
|
-
if (file) {
|
|
1708
|
-
const fileName = file instanceof File ? file.name : "game.zip";
|
|
1709
|
-
const initiateResponse = await client["request"]("/games/uploads/initiate/", "POST", {
|
|
1710
|
-
body: {
|
|
1711
|
-
fileName,
|
|
1712
|
-
gameId: game?.id || slug
|
|
1713
|
-
}
|
|
1714
|
-
});
|
|
1715
|
-
uploadToken = initiateResponse.uploadToken;
|
|
1716
|
-
if (hooks?.onEvent && typeof XMLHttpRequest !== "undefined") {
|
|
1717
|
-
await new Promise((resolve, reject) => {
|
|
1718
|
-
const xhr = new XMLHttpRequest;
|
|
1719
|
-
xhr.open("PUT", initiateResponse.presignedUrl, true);
|
|
1720
|
-
const contentType = file.type || "application/octet-stream";
|
|
1721
|
-
try {
|
|
1722
|
-
xhr.setRequestHeader("Content-Type", contentType);
|
|
1723
|
-
} catch {}
|
|
1724
|
-
xhr.upload.onprogress = (event) => {
|
|
1725
|
-
if (event.lengthComputable) {
|
|
1726
|
-
const percent = event.loaded / event.total;
|
|
1727
|
-
hooks.onEvent?.({
|
|
1728
|
-
type: "s3Progress",
|
|
1729
|
-
loaded: event.loaded,
|
|
1730
|
-
total: event.total,
|
|
1731
|
-
percent
|
|
1732
|
-
});
|
|
1733
|
-
}
|
|
1734
|
-
};
|
|
1735
|
-
xhr.onload = () => {
|
|
1736
|
-
if (xhr.status >= 200 && xhr.status < 300)
|
|
1737
|
-
resolve();
|
|
1738
|
-
else
|
|
1739
|
-
reject(new Error(`File upload failed: ${xhr.status} ${xhr.statusText}`));
|
|
1740
|
-
};
|
|
1741
|
-
xhr.onerror = () => reject(new Error("File upload failed: network error"));
|
|
1742
|
-
xhr.send(file);
|
|
1743
|
-
});
|
|
1744
|
-
} else {
|
|
1745
|
-
const uploadResponse = await fetch(initiateResponse.presignedUrl, {
|
|
1746
|
-
method: "PUT",
|
|
1747
|
-
body: file,
|
|
1748
|
-
headers: {
|
|
1749
|
-
"Content-Type": file.type || "application/octet-stream"
|
|
1750
|
-
}
|
|
1751
|
-
});
|
|
1752
|
-
if (!uploadResponse.ok) {
|
|
1753
|
-
throw new Error(`File upload failed: ${uploadResponse.status} ${uploadResponse.statusText}`);
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1901
|
+
const uploadToken = file ? await deploy.uploadFile(file, game?.id || slug, hooks) : undefined;
|
|
1757
1902
|
if (uploadToken || backend) {
|
|
1758
|
-
|
|
1759
|
-
if (uploadToken)
|
|
1760
|
-
requestBody.uploadToken = uploadToken;
|
|
1761
|
-
if (metadata)
|
|
1762
|
-
requestBody.metadata = metadata;
|
|
1763
|
-
if (backend) {
|
|
1764
|
-
requestBody.code = backend.code;
|
|
1765
|
-
requestBody.config = backend.config;
|
|
1766
|
-
if (backend.bindings)
|
|
1767
|
-
requestBody.bindings = backend.bindings;
|
|
1768
|
-
if (backend.schema)
|
|
1769
|
-
requestBody.schema = backend.schema;
|
|
1770
|
-
}
|
|
1771
|
-
const job = await client["request"](`/games/${slug}/deploy`, "POST", {
|
|
1772
|
-
body: requestBody
|
|
1773
|
-
});
|
|
1774
|
-
const completedJob = await pollDeployJob(slug, job.id, hooks);
|
|
1775
|
-
if (!completedJob.result?.url) {
|
|
1776
|
-
throw new Error("Deployment completed but no deployment URL was recorded");
|
|
1777
|
-
}
|
|
1778
|
-
for (let attempt = 0;attempt < 3; attempt++) {
|
|
1779
|
-
try {
|
|
1780
|
-
return await client["request"](`/games/${slug}`, "GET");
|
|
1781
|
-
} catch {
|
|
1782
|
-
if (attempt < 2) {
|
|
1783
|
-
await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
throw new Error("Deploy succeeded but failed to fetch updated game after 3 attempts");
|
|
1903
|
+
return deploy.submit({ slug, game, uploadToken, metadata, backend, hooks });
|
|
1788
1904
|
}
|
|
1789
1905
|
if (game) {
|
|
1790
1906
|
return game;
|
|
1791
1907
|
}
|
|
1792
1908
|
throw new Error("No deployment actions specified (need metadata, file, or backend)");
|
|
1793
1909
|
},
|
|
1794
|
-
seed: async (slug, code, environment, secrets) => {
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
});
|
|
1798
|
-
},
|
|
1910
|
+
seed: async (slug, code, environment, secrets) => client["request"](`/games/${slug}/seed`, "POST", {
|
|
1911
|
+
body: { code, environment, secrets }
|
|
1912
|
+
}),
|
|
1799
1913
|
upsert: async (slug, metadata) => client["request"](`/games/${slug}`, "PUT", { body: metadata }),
|
|
1800
1914
|
delete: (gameId) => client["request"](`/games/${gameId}`, "DELETE"),
|
|
1801
1915
|
secrets: {
|
|
@@ -1812,11 +1926,9 @@ function createDevNamespace(client) {
|
|
|
1812
1926
|
}
|
|
1813
1927
|
},
|
|
1814
1928
|
database: {
|
|
1815
|
-
reset: async (slug, schema) => {
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
});
|
|
1819
|
-
}
|
|
1929
|
+
reset: async (slug, schema) => client["request"](`/games/${slug}/database/reset`, "POST", {
|
|
1930
|
+
body: { schema }
|
|
1931
|
+
})
|
|
1820
1932
|
},
|
|
1821
1933
|
bucket: {
|
|
1822
1934
|
list: async (slug, prefix) => {
|
|
@@ -1868,9 +1980,7 @@ function createDevNamespace(client) {
|
|
|
1868
1980
|
const result = await client["request"](`/games/${slug}/kv/${encodeURIComponent(key)}/metadata`, "GET");
|
|
1869
1981
|
return result.metadata;
|
|
1870
1982
|
},
|
|
1871
|
-
stats: async (slug) => {
|
|
1872
|
-
return client["request"](`/games/${slug}/kv/stats`, "GET");
|
|
1873
|
-
},
|
|
1983
|
+
stats: async (slug) => client["request"](`/games/${slug}/kv/stats`, "GET"),
|
|
1874
1984
|
seed: async (slug, entries) => {
|
|
1875
1985
|
const result = await client["request"](`/games/${slug}/kv/bulk`, "PUT", { body: { entries } });
|
|
1876
1986
|
return result.count;
|
|
@@ -1881,11 +1991,9 @@ function createDevNamespace(client) {
|
|
|
1881
1991
|
}
|
|
1882
1992
|
},
|
|
1883
1993
|
domains: {
|
|
1884
|
-
add: async (slug, hostname) => {
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
});
|
|
1888
|
-
},
|
|
1994
|
+
add: async (slug, hostname) => client["request"](`/games/${slug}/domains`, "POST", {
|
|
1995
|
+
body: { hostname }
|
|
1996
|
+
}),
|
|
1889
1997
|
list: async (slug) => {
|
|
1890
1998
|
const result = await client["request"](`/games/${slug}/domains`, "GET");
|
|
1891
1999
|
return result.domains;
|
|
@@ -1899,9 +2007,7 @@ function createDevNamespace(client) {
|
|
|
1899
2007
|
}
|
|
1900
2008
|
},
|
|
1901
2009
|
logs: {
|
|
1902
|
-
getToken: async (slug, environment) => {
|
|
1903
|
-
return client["request"](`/games/${slug}/logs/token`, "POST", { body: { environment } });
|
|
1904
|
-
}
|
|
2010
|
+
getToken: async (slug, environment) => client["request"](`/games/${slug}/logs/token`, "POST", { body: { environment } })
|
|
1905
2011
|
}
|
|
1906
2012
|
},
|
|
1907
2013
|
items: {
|
|
@@ -1921,48 +2027,44 @@ function createDevNamespace(client) {
|
|
|
1921
2027
|
},
|
|
1922
2028
|
delete: (gameId, itemId) => client["request"](`/games/${gameId}/items/${itemId}`, "DELETE"),
|
|
1923
2029
|
shop: {
|
|
1924
|
-
create: (gameId, itemId, listingData) => {
|
|
1925
|
-
|
|
1926
|
-
},
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
},
|
|
1930
|
-
update: (gameId, itemId, updates) => {
|
|
1931
|
-
return client["request"](`/games/${gameId}/items/${itemId}/shop-listing`, "PATCH", { body: updates });
|
|
1932
|
-
},
|
|
1933
|
-
delete: (gameId, itemId) => {
|
|
1934
|
-
return client["request"](`/games/${gameId}/items/${itemId}/shop-listing`, "DELETE");
|
|
1935
|
-
},
|
|
1936
|
-
list: (gameId) => {
|
|
1937
|
-
return client["request"](`/games/${gameId}/shop-listings`, "GET");
|
|
1938
|
-
}
|
|
2030
|
+
create: (gameId, itemId, listingData) => client["request"](`/games/${gameId}/items/${itemId}/shop-listing`, "POST", { body: listingData }),
|
|
2031
|
+
get: (gameId, itemId) => client["request"](`/games/${gameId}/items/${itemId}/shop-listing`, "GET"),
|
|
2032
|
+
update: (gameId, itemId, updates) => client["request"](`/games/${gameId}/items/${itemId}/shop-listing`, "PATCH", { body: updates }),
|
|
2033
|
+
delete: (gameId, itemId) => client["request"](`/games/${gameId}/items/${itemId}/shop-listing`, "DELETE"),
|
|
2034
|
+
list: (gameId) => client["request"](`/games/${gameId}/shop-listings`, "GET")
|
|
1939
2035
|
}
|
|
1940
2036
|
}
|
|
1941
2037
|
};
|
|
1942
2038
|
}
|
|
1943
2039
|
// src/core/request.ts
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
2040
|
+
var RETRY_DELAYS_MS = [500, 1500];
|
|
2041
|
+
function wait(ms) {
|
|
2042
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2043
|
+
}
|
|
2044
|
+
var retryRuntime = { wait };
|
|
2045
|
+
function isRetryableStatus(status) {
|
|
2046
|
+
return status === 429 || status >= 500;
|
|
2047
|
+
}
|
|
2048
|
+
async function fetchWithRetry(url, init2) {
|
|
2049
|
+
for (let attempt = 0;attempt <= RETRY_DELAYS_MS.length; attempt++) {
|
|
2050
|
+
const retryDelayMs = RETRY_DELAYS_MS[attempt];
|
|
2051
|
+
const canRetry = init2.method === "GET" && retryDelayMs !== undefined;
|
|
2052
|
+
try {
|
|
2053
|
+
const response = await fetch(url, init2);
|
|
2054
|
+
if (canRetry && isRetryableStatus(response.status)) {
|
|
2055
|
+
await retryRuntime.wait(retryDelayMs);
|
|
2056
|
+
} else {
|
|
2057
|
+
return response;
|
|
2058
|
+
}
|
|
2059
|
+
} catch (error) {
|
|
2060
|
+
if (canRetry && error instanceof TypeError) {
|
|
2061
|
+
await retryRuntime.wait(retryDelayMs);
|
|
2062
|
+
} else {
|
|
2063
|
+
throw error;
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
1965
2066
|
}
|
|
2067
|
+
throw new PlaycademyError("Request failed after exhausting retries");
|
|
1966
2068
|
}
|
|
1967
2069
|
function prepareRequestBody(body, headers) {
|
|
1968
2070
|
if (body instanceof FormData) {
|
|
@@ -1983,6 +2085,33 @@ function prepareRequestBody(body, headers) {
|
|
|
1983
2085
|
}
|
|
1984
2086
|
return;
|
|
1985
2087
|
}
|
|
2088
|
+
function checkDevWarnings(data) {
|
|
2089
|
+
if (!data || typeof data !== "object") {
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
const response = data;
|
|
2093
|
+
const warningType = response.__playcademyDevWarning;
|
|
2094
|
+
if (!warningType) {
|
|
2095
|
+
return;
|
|
2096
|
+
}
|
|
2097
|
+
switch (warningType) {
|
|
2098
|
+
case "timeback-not-configured": {
|
|
2099
|
+
console.warn("%c⚠️ TimeBack Not Configured", "background: #f59e0b; color: white; padding: 6px 12px; border-radius: 4px; font-weight: bold; font-size: 13px");
|
|
2100
|
+
console.log("%cTimeBack is configured in playcademy.config.js but the sandbox does not have TimeBack credentials.", "color: #f59e0b; font-weight: 500");
|
|
2101
|
+
console.log("To test TimeBack locally:");
|
|
2102
|
+
console.log(" Set the following environment variables:");
|
|
2103
|
+
console.log(" • %cTIMEBACK_ONEROSTER_API_URL", "color: #0ea5e9; font-weight: 600; font-family: monospace");
|
|
2104
|
+
console.log(" • %cTIMEBACK_CALIPER_API_URL", "color: #0ea5e9; font-weight: 600; font-family: monospace");
|
|
2105
|
+
console.log(" • %cTIMEBACK_API_CLIENT_ID/SECRET", "color: #0ea5e9; font-weight: 600; font-family: monospace");
|
|
2106
|
+
console.log(" Or deploy your game: %cplaycademy deploy", "color: #10b981; font-weight: 600; font-family: monospace");
|
|
2107
|
+
console.log(" Or wait for %c@superbuilders/timeback-local%c (coming soon)", "color: #8b5cf6; font-weight: 600; font-family: monospace", "color: inherit");
|
|
2108
|
+
break;
|
|
2109
|
+
}
|
|
2110
|
+
default: {
|
|
2111
|
+
console.warn(`[Playcademy Dev Warning] ${warningType}`);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
1986
2115
|
async function request({
|
|
1987
2116
|
path,
|
|
1988
2117
|
baseUrl,
|
|
@@ -1994,7 +2123,7 @@ async function request({
|
|
|
1994
2123
|
const url = baseUrl.replace(/\/$/, "") + (path.startsWith("/") ? path : `/${path}`);
|
|
1995
2124
|
const headers = { ...extraHeaders };
|
|
1996
2125
|
const payload = prepareRequestBody(body, headers);
|
|
1997
|
-
const res = await
|
|
2126
|
+
const res = await fetchWithRetry(url, {
|
|
1998
2127
|
method,
|
|
1999
2128
|
headers,
|
|
2000
2129
|
body: payload,
|
|
@@ -2010,18 +2139,20 @@ async function request({
|
|
|
2010
2139
|
})) ?? undefined;
|
|
2011
2140
|
throw ApiError.fromResponse(res.status, res.statusText, errorBody);
|
|
2012
2141
|
}
|
|
2013
|
-
if (res.status === 204)
|
|
2142
|
+
if (res.status === 204) {
|
|
2014
2143
|
return;
|
|
2144
|
+
}
|
|
2015
2145
|
const contentType = res.headers.get("content-type") ?? "";
|
|
2016
2146
|
if (contentType.includes("application/json")) {
|
|
2017
2147
|
try {
|
|
2018
2148
|
const parsed = await res.json();
|
|
2019
2149
|
checkDevWarnings(parsed);
|
|
2020
2150
|
return parsed;
|
|
2021
|
-
} catch (
|
|
2022
|
-
if (
|
|
2151
|
+
} catch (error) {
|
|
2152
|
+
if (error instanceof SyntaxError) {
|
|
2023
2153
|
return;
|
|
2024
|
-
|
|
2154
|
+
}
|
|
2155
|
+
throw error;
|
|
2025
2156
|
}
|
|
2026
2157
|
}
|
|
2027
2158
|
const rawText = await res.text().catch(() => "");
|
|
@@ -2029,21 +2160,26 @@ async function request({
|
|
|
2029
2160
|
}
|
|
2030
2161
|
async function fetchManifest(deploymentUrl) {
|
|
2031
2162
|
const manifestUrl = `${deploymentUrl.replace(/\/$/, "")}/playcademy.manifest.json`;
|
|
2163
|
+
let response;
|
|
2164
|
+
try {
|
|
2165
|
+
response = await fetchWithRetry(manifestUrl, { method: "GET" });
|
|
2166
|
+
} catch (error) {
|
|
2167
|
+
log.error(`[Playcademy SDK] Error fetching manifest from ${manifestUrl}:`, {
|
|
2168
|
+
error
|
|
2169
|
+
});
|
|
2170
|
+
throw new ManifestError("Failed to load game manifest", "temporary");
|
|
2171
|
+
}
|
|
2172
|
+
if (!response.ok) {
|
|
2173
|
+
log.error(`[fetchManifest] Failed to fetch manifest from ${manifestUrl}. Status: ${response.status}`);
|
|
2174
|
+
throw new ManifestError(`Failed to fetch manifest: ${response.status} ${response.statusText}`, isRetryableStatus(response.status) ? "temporary" : "permanent");
|
|
2175
|
+
}
|
|
2032
2176
|
try {
|
|
2033
|
-
const response = await fetch(manifestUrl);
|
|
2034
|
-
if (!response.ok) {
|
|
2035
|
-
log.error(`[fetchManifest] Failed to fetch manifest from ${manifestUrl}. Status: ${response.status}`);
|
|
2036
|
-
throw new PlaycademyError(`Failed to fetch manifest: ${response.status} ${response.statusText}`);
|
|
2037
|
-
}
|
|
2038
2177
|
return await response.json();
|
|
2039
2178
|
} catch (error) {
|
|
2040
|
-
|
|
2041
|
-
throw error;
|
|
2042
|
-
}
|
|
2043
|
-
log.error(`[Playcademy SDK] Error fetching or parsing manifest from ${manifestUrl}:`, {
|
|
2179
|
+
log.error(`[Playcademy SDK] Error parsing manifest from ${manifestUrl}:`, {
|
|
2044
2180
|
error
|
|
2045
2181
|
});
|
|
2046
|
-
throw new
|
|
2182
|
+
throw new ManifestError("Failed to parse game manifest", "permanent");
|
|
2047
2183
|
}
|
|
2048
2184
|
}
|
|
2049
2185
|
|
|
@@ -2069,12 +2205,8 @@ function createGamesNamespace(client) {
|
|
|
2069
2205
|
return baseGameData;
|
|
2070
2206
|
}, options);
|
|
2071
2207
|
},
|
|
2072
|
-
list: (options) =>
|
|
2073
|
-
|
|
2074
|
-
},
|
|
2075
|
-
getSubjects: () => {
|
|
2076
|
-
return client["request"]("/games/subjects", "GET");
|
|
2077
|
-
},
|
|
2208
|
+
list: (options) => gamesListCache.get("all", () => client["request"]("/games", "GET"), options),
|
|
2209
|
+
getSubjects: () => client["request"]("/games/subjects", "GET"),
|
|
2078
2210
|
startSession: async (gameId) => {
|
|
2079
2211
|
const idToUse = gameId ?? client["_ensureGameId"]();
|
|
2080
2212
|
return client["request"](`/games/${idToUse}/sessions`, "POST", {});
|
|
@@ -2098,10 +2230,12 @@ function createGamesNamespace(client) {
|
|
|
2098
2230
|
leaderboard: {
|
|
2099
2231
|
get: async (gameId, options) => {
|
|
2100
2232
|
const params = new URLSearchParams;
|
|
2101
|
-
if (options?.limit)
|
|
2233
|
+
if (options?.limit) {
|
|
2102
2234
|
params.append("limit", String(options.limit));
|
|
2103
|
-
|
|
2235
|
+
}
|
|
2236
|
+
if (options?.offset) {
|
|
2104
2237
|
params.append("offset", String(options.offset));
|
|
2238
|
+
}
|
|
2105
2239
|
const queryString = params.toString();
|
|
2106
2240
|
const path = queryString ? `/games/${gameId}/leaderboard?${queryString}` : `/games/${gameId}/leaderboard`;
|
|
2107
2241
|
return client["request"](path, "GET");
|
|
@@ -2129,14 +2263,10 @@ function createCharacterNamespace(client) {
|
|
|
2129
2263
|
throw error;
|
|
2130
2264
|
}
|
|
2131
2265
|
},
|
|
2132
|
-
create: async (characterData) => {
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
},
|
|
2137
|
-
update: async (updates) => {
|
|
2138
|
-
return client["request"]("/character", "PATCH", { body: updates });
|
|
2139
|
-
},
|
|
2266
|
+
create: async (characterData) => client["request"]("/character", "POST", {
|
|
2267
|
+
body: characterData
|
|
2268
|
+
}),
|
|
2269
|
+
update: async (updates) => client["request"]("/character", "PATCH", { body: updates }),
|
|
2140
2270
|
components: {
|
|
2141
2271
|
list: async (options) => {
|
|
2142
2272
|
const cacheKey = options?.level === undefined ? "all" : String(options.level);
|
|
@@ -2150,12 +2280,8 @@ function createCharacterNamespace(client) {
|
|
|
2150
2280
|
getCacheKeys: () => componentCache.getKeys()
|
|
2151
2281
|
},
|
|
2152
2282
|
accessories: {
|
|
2153
|
-
equip: async (slot, componentId) => {
|
|
2154
|
-
|
|
2155
|
-
},
|
|
2156
|
-
remove: async (slot) => {
|
|
2157
|
-
return client["request"](`/character/accessories/${slot}`, "DELETE");
|
|
2158
|
-
},
|
|
2283
|
+
equip: async (slot, componentId) => client["request"]("/character/accessories/equip", "POST", { body: { slot, accessoryComponentId: componentId } }),
|
|
2284
|
+
remove: async (slot) => client["request"](`/character/accessories/${slot}`, "DELETE"),
|
|
2159
2285
|
list: async () => {
|
|
2160
2286
|
const character2 = await client.character.get();
|
|
2161
2287
|
return character2?.accessories || [];
|
|
@@ -2174,14 +2300,13 @@ function createAchievementsNamespace(client) {
|
|
|
2174
2300
|
keyPrefix: "achievements.history"
|
|
2175
2301
|
});
|
|
2176
2302
|
return {
|
|
2177
|
-
list: (options) =>
|
|
2178
|
-
return achievementsListCache.get("current", () => client["request"]("/achievements/current", "GET"), options);
|
|
2179
|
-
},
|
|
2303
|
+
list: (options) => achievementsListCache.get("current", () => client["request"]("/achievements/current", "GET"), options),
|
|
2180
2304
|
history: {
|
|
2181
2305
|
list: async (queryOptions, cacheOptions) => {
|
|
2182
2306
|
const params = new URLSearchParams;
|
|
2183
|
-
if (queryOptions?.limit)
|
|
2307
|
+
if (queryOptions?.limit) {
|
|
2184
2308
|
params.append("limit", String(queryOptions.limit));
|
|
2309
|
+
}
|
|
2185
2310
|
const qs = params.toString();
|
|
2186
2311
|
const path = qs ? `/achievements/history?${qs}` : "/achievements/history";
|
|
2187
2312
|
const cacheKey = qs ? `history-${qs}` : "history";
|
|
@@ -2209,9 +2334,7 @@ function createLeaderboardNamespace(client) {
|
|
|
2209
2334
|
}
|
|
2210
2335
|
return client["request"](`/leaderboard?${params}`, "GET");
|
|
2211
2336
|
},
|
|
2212
|
-
getUserRank: async (gameId, userId) => {
|
|
2213
|
-
return client["request"](`/games/${gameId}/users/${userId}/rank`, "GET");
|
|
2214
|
-
}
|
|
2337
|
+
getUserRank: async (gameId, userId) => client["request"](`/games/${gameId}/users/${userId}/rank`, "GET")
|
|
2215
2338
|
};
|
|
2216
2339
|
}
|
|
2217
2340
|
// src/core/cache/cooldown-cache.ts
|
|
@@ -2265,28 +2388,18 @@ function createCooldownCache(defaultCooldownMs) {
|
|
|
2265
2388
|
function createLevelsNamespace(client) {
|
|
2266
2389
|
const progressCache = createCooldownCache(5000);
|
|
2267
2390
|
return {
|
|
2268
|
-
get: async () =>
|
|
2269
|
-
|
|
2270
|
-
},
|
|
2271
|
-
progress: async (options) => {
|
|
2272
|
-
return progressCache.get("user-progress", () => client["request"]("/users/level/progress", "GET"), options);
|
|
2273
|
-
},
|
|
2391
|
+
get: async () => client["request"]("/users/level", "GET"),
|
|
2392
|
+
progress: async (options) => progressCache.get("user-progress", () => client["request"]("/users/level/progress", "GET"), options),
|
|
2274
2393
|
config: {
|
|
2275
|
-
list: async () =>
|
|
2276
|
-
|
|
2277
|
-
},
|
|
2278
|
-
get: async (level) => {
|
|
2279
|
-
return client["request"](`/levels/config/${level}`, "GET");
|
|
2280
|
-
}
|
|
2394
|
+
list: async () => client["request"]("/levels/config", "GET"),
|
|
2395
|
+
get: async (level) => client["request"](`/levels/config/${level}`, "GET")
|
|
2281
2396
|
}
|
|
2282
2397
|
};
|
|
2283
2398
|
}
|
|
2284
2399
|
// src/namespaces/platform/shop.ts
|
|
2285
2400
|
function createShopNamespace(client) {
|
|
2286
2401
|
return {
|
|
2287
|
-
view: () =>
|
|
2288
|
-
return client["request"]("/shop/view", "GET");
|
|
2289
|
-
}
|
|
2402
|
+
view: () => client["request"]("/shop/view", "GET")
|
|
2290
2403
|
};
|
|
2291
2404
|
}
|
|
2292
2405
|
// src/namespaces/platform/notifications.ts
|
|
@@ -2302,14 +2415,18 @@ function createNotificationsNamespace(client) {
|
|
|
2302
2415
|
return {
|
|
2303
2416
|
list: async (queryOptions, cacheOptions) => {
|
|
2304
2417
|
const params = new URLSearchParams;
|
|
2305
|
-
if (queryOptions?.status)
|
|
2418
|
+
if (queryOptions?.status) {
|
|
2306
2419
|
params.append("status", queryOptions.status);
|
|
2307
|
-
|
|
2420
|
+
}
|
|
2421
|
+
if (queryOptions?.type) {
|
|
2308
2422
|
params.append("type", queryOptions.type);
|
|
2309
|
-
|
|
2423
|
+
}
|
|
2424
|
+
if (queryOptions?.limit) {
|
|
2310
2425
|
params.append("limit", String(queryOptions.limit));
|
|
2311
|
-
|
|
2426
|
+
}
|
|
2427
|
+
if (queryOptions?.offset) {
|
|
2312
2428
|
params.append("offset", String(queryOptions.offset));
|
|
2429
|
+
}
|
|
2313
2430
|
const qs = params.toString();
|
|
2314
2431
|
const path = qs ? `/notifications?${qs}` : "/notifications";
|
|
2315
2432
|
const cacheKey = qs ? `list-${qs}` : "list";
|
|
@@ -2365,10 +2482,12 @@ function createNotificationsNamespace(client) {
|
|
|
2365
2482
|
get: async (queryOptions, cacheOptions) => {
|
|
2366
2483
|
const user = await client.users.me();
|
|
2367
2484
|
const params = new URLSearchParams;
|
|
2368
|
-
if (queryOptions?.from)
|
|
2485
|
+
if (queryOptions?.from) {
|
|
2369
2486
|
params.append("from", queryOptions.from);
|
|
2370
|
-
|
|
2487
|
+
}
|
|
2488
|
+
if (queryOptions?.to) {
|
|
2371
2489
|
params.append("to", queryOptions.to);
|
|
2490
|
+
}
|
|
2372
2491
|
const qs = params.toString();
|
|
2373
2492
|
const path = qs ? `/notifications/stats/${user.id}?${qs}` : `/notifications/stats/${user.id}`;
|
|
2374
2493
|
const cacheKey = qs ? `stats-${qs}` : "stats";
|
|
@@ -2405,11 +2524,10 @@ function createSpritesNamespace(client) {
|
|
|
2405
2524
|
return {
|
|
2406
2525
|
templates: {
|
|
2407
2526
|
get: async (slug) => {
|
|
2408
|
-
if (!slug)
|
|
2527
|
+
if (!slug) {
|
|
2409
2528
|
throw new Error("Sprite template slug is required");
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
});
|
|
2529
|
+
}
|
|
2530
|
+
const templateMeta = await templateUrlCache.get(slug, async () => client["request"](`/sprites/templates/${slug}`, "GET"));
|
|
2413
2531
|
if (!templateMeta.url) {
|
|
2414
2532
|
throw new Error(`Template ${slug} has no URL in database`);
|
|
2415
2533
|
}
|
|
@@ -2459,7 +2577,9 @@ function createTimebackNamespace2(client) {
|
|
|
2459
2577
|
ttl: 30 * 1000,
|
|
2460
2578
|
keyPrefix: "platform.timeback.students"
|
|
2461
2579
|
});
|
|
2462
|
-
|
|
2580
|
+
function getTimeback() {
|
|
2581
|
+
return client["initPayload"]?.timeback;
|
|
2582
|
+
}
|
|
2463
2583
|
return {
|
|
2464
2584
|
get user() {
|
|
2465
2585
|
return {
|
|
@@ -2475,26 +2595,22 @@ function createTimebackNamespace2(client) {
|
|
|
2475
2595
|
get organizations() {
|
|
2476
2596
|
return getTimeback()?.organizations ?? [];
|
|
2477
2597
|
},
|
|
2478
|
-
fetch: async (options) => {
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
}, options);
|
|
2492
|
-
}
|
|
2598
|
+
fetch: async (options) => userCache.get("current", async () => {
|
|
2599
|
+
const response = await client["request"]("/timeback/user", "GET");
|
|
2600
|
+
const initPayload = client["initPayload"];
|
|
2601
|
+
if (initPayload) {
|
|
2602
|
+
initPayload.timeback = response;
|
|
2603
|
+
}
|
|
2604
|
+
return {
|
|
2605
|
+
id: response.id,
|
|
2606
|
+
role: response.role,
|
|
2607
|
+
enrollments: response.enrollments,
|
|
2608
|
+
organizations: response.organizations
|
|
2609
|
+
};
|
|
2610
|
+
}, options)
|
|
2493
2611
|
};
|
|
2494
2612
|
},
|
|
2495
|
-
populateStudent: async (names) => {
|
|
2496
|
-
return client["request"]("/timeback/populate-student", "POST", names ? { body: names } : undefined);
|
|
2497
|
-
},
|
|
2613
|
+
populateStudent: async (names) => client["request"]("/timeback/populate-student", "POST", names ? { body: names } : undefined),
|
|
2498
2614
|
startActivity: (_metadata) => {
|
|
2499
2615
|
throw new Error(NOT_SUPPORTED);
|
|
2500
2616
|
},
|
|
@@ -2508,44 +2624,36 @@ function createTimebackNamespace2(client) {
|
|
|
2508
2624
|
throw new Error(NOT_SUPPORTED);
|
|
2509
2625
|
},
|
|
2510
2626
|
management: {
|
|
2511
|
-
setup: (request2) => {
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
},
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
},
|
|
2519
|
-
cleanup: (gameId) => {
|
|
2520
|
-
return client["request"](`/timeback/integrations/${gameId}`, "DELETE");
|
|
2521
|
-
},
|
|
2522
|
-
get: (gameId) => {
|
|
2523
|
-
return client["request"](`/timeback/integrations/${gameId}`, "GET");
|
|
2524
|
-
},
|
|
2525
|
-
getConfig: (gameId) => {
|
|
2526
|
-
return client["request"](`/timeback/config/${gameId}`, "GET");
|
|
2527
|
-
}
|
|
2627
|
+
setup: (request2) => client["request"]("/timeback/setup", "POST", {
|
|
2628
|
+
body: request2
|
|
2629
|
+
}),
|
|
2630
|
+
verify: (gameId) => client["request"](`/timeback/verify/${gameId}`, "GET"),
|
|
2631
|
+
cleanup: (gameId) => client["request"](`/timeback/integrations/${gameId}`, "DELETE"),
|
|
2632
|
+
get: (gameId) => client["request"](`/timeback/integrations/${gameId}`, "GET"),
|
|
2633
|
+
getConfig: (gameId) => client["request"](`/timeback/config/${gameId}`, "GET")
|
|
2528
2634
|
},
|
|
2529
2635
|
xp: {
|
|
2530
2636
|
today: async (options) => {
|
|
2531
2637
|
const params = new URLSearchParams;
|
|
2532
|
-
if (options?.date)
|
|
2638
|
+
if (options?.date) {
|
|
2533
2639
|
params.set("date", options.date);
|
|
2534
|
-
|
|
2640
|
+
}
|
|
2641
|
+
if (options?.timezone) {
|
|
2535
2642
|
params.set("tz", options.timezone);
|
|
2643
|
+
}
|
|
2536
2644
|
const query = params.toString();
|
|
2537
2645
|
const endpoint = query ? `/timeback/xp/today?${query}` : "/timeback/xp/today";
|
|
2538
2646
|
return client["request"](endpoint, "GET");
|
|
2539
2647
|
},
|
|
2540
|
-
total: async () =>
|
|
2541
|
-
return client["request"]("/timeback/xp/total", "GET");
|
|
2542
|
-
},
|
|
2648
|
+
total: async () => client["request"]("/timeback/xp/total", "GET"),
|
|
2543
2649
|
history: async (options) => {
|
|
2544
2650
|
const params = new URLSearchParams;
|
|
2545
|
-
if (options?.startDate)
|
|
2651
|
+
if (options?.startDate) {
|
|
2546
2652
|
params.set("startDate", options.startDate);
|
|
2547
|
-
|
|
2653
|
+
}
|
|
2654
|
+
if (options?.endDate) {
|
|
2548
2655
|
params.set("endDate", options.endDate);
|
|
2656
|
+
}
|
|
2549
2657
|
const query = params.toString();
|
|
2550
2658
|
const endpoint = query ? `/timeback/xp/history?${query}` : "/timeback/xp/history";
|
|
2551
2659
|
return client["request"](endpoint, "GET");
|
|
@@ -2554,10 +2662,12 @@ function createTimebackNamespace2(client) {
|
|
|
2554
2662
|
const [today, total] = await Promise.all([
|
|
2555
2663
|
client["request"]((() => {
|
|
2556
2664
|
const params = new URLSearchParams;
|
|
2557
|
-
if (options?.date)
|
|
2665
|
+
if (options?.date) {
|
|
2558
2666
|
params.set("date", options.date);
|
|
2559
|
-
|
|
2667
|
+
}
|
|
2668
|
+
if (options?.timezone) {
|
|
2560
2669
|
params.set("tz", options.timezone);
|
|
2670
|
+
}
|
|
2561
2671
|
const query = params.toString();
|
|
2562
2672
|
return query ? `/timeback/xp/today?${query}` : "/timeback/xp/today";
|
|
2563
2673
|
})(), "GET"),
|
|
@@ -2567,11 +2677,7 @@ function createTimebackNamespace2(client) {
|
|
|
2567
2677
|
}
|
|
2568
2678
|
},
|
|
2569
2679
|
students: {
|
|
2570
|
-
get: async (timebackId, options) => {
|
|
2571
|
-
return studentCache.get(timebackId, async () => {
|
|
2572
|
-
return client["request"](`/timeback/user/${timebackId}`, "GET");
|
|
2573
|
-
}, options);
|
|
2574
|
-
},
|
|
2680
|
+
get: async (timebackId, options) => studentCache.get(timebackId, async () => client["request"](`/timeback/user/${timebackId}`, "GET"), options),
|
|
2575
2681
|
clearCache: (timebackId) => {
|
|
2576
2682
|
studentCache.clear(timebackId);
|
|
2577
2683
|
}
|
|
@@ -2677,24 +2783,26 @@ class ConnectionMonitor {
|
|
|
2677
2783
|
this._detectInitialState();
|
|
2678
2784
|
}
|
|
2679
2785
|
start() {
|
|
2680
|
-
if (this.isMonitoring)
|
|
2786
|
+
if (this.isMonitoring) {
|
|
2681
2787
|
return;
|
|
2788
|
+
}
|
|
2682
2789
|
this.isMonitoring = true;
|
|
2683
|
-
if (this.config.enableOfflineEvents && typeof window !== "undefined") {
|
|
2684
|
-
|
|
2685
|
-
|
|
2790
|
+
if (this.config.enableOfflineEvents && typeof globalThis.window !== "undefined") {
|
|
2791
|
+
globalThis.addEventListener("online", this._handleOnline);
|
|
2792
|
+
globalThis.addEventListener("offline", this._handleOffline);
|
|
2686
2793
|
}
|
|
2687
2794
|
if (this.config.enableHeartbeat) {
|
|
2688
2795
|
this._startHeartbeat();
|
|
2689
2796
|
}
|
|
2690
2797
|
}
|
|
2691
2798
|
stop() {
|
|
2692
|
-
if (!this.isMonitoring)
|
|
2799
|
+
if (!this.isMonitoring) {
|
|
2693
2800
|
return;
|
|
2801
|
+
}
|
|
2694
2802
|
this.isMonitoring = false;
|
|
2695
|
-
if (typeof window !== "undefined") {
|
|
2696
|
-
|
|
2697
|
-
|
|
2803
|
+
if (typeof globalThis.window !== "undefined") {
|
|
2804
|
+
globalThis.removeEventListener("online", this._handleOnline);
|
|
2805
|
+
globalThis.removeEventListener("offline", this._handleOffline);
|
|
2698
2806
|
}
|
|
2699
2807
|
if (this.heartbeatInterval) {
|
|
2700
2808
|
clearInterval(this.heartbeatInterval);
|
|
@@ -2714,8 +2822,9 @@ class ConnectionMonitor {
|
|
|
2714
2822
|
}
|
|
2715
2823
|
reportRequestFailure(error) {
|
|
2716
2824
|
const isNetworkError = error instanceof TypeError || error instanceof Error && error.message.includes("fetch");
|
|
2717
|
-
if (!isNetworkError)
|
|
2825
|
+
if (!isNetworkError) {
|
|
2718
2826
|
return;
|
|
2827
|
+
}
|
|
2719
2828
|
this.consecutiveFailures++;
|
|
2720
2829
|
if (this.consecutiveFailures >= this.config.failureThreshold) {
|
|
2721
2830
|
this._setState("degraded", "Multiple consecutive request failures");
|
|
@@ -2783,8 +2892,9 @@ class ConnectionMonitor {
|
|
|
2783
2892
|
}
|
|
2784
2893
|
}
|
|
2785
2894
|
_setState(newState, reason) {
|
|
2786
|
-
if (this.state === newState)
|
|
2895
|
+
if (this.state === newState) {
|
|
2787
2896
|
return;
|
|
2897
|
+
}
|
|
2788
2898
|
const oldState = this.state;
|
|
2789
2899
|
this.state = newState;
|
|
2790
2900
|
console.debug(`[ConnectionMonitor] ${oldState} → ${newState}: ${reason}`);
|
|
@@ -2800,14 +2910,15 @@ class ConnectionMonitor {
|
|
|
2800
2910
|
// src/core/connection/utils.ts
|
|
2801
2911
|
function createDisplayAlert(authContext) {
|
|
2802
2912
|
return (message, options) => {
|
|
2803
|
-
if (authContext?.isInIframe && typeof window !== "undefined" && window.parent !== window) {
|
|
2913
|
+
if (authContext?.isInIframe && typeof globalThis.window !== "undefined" && globalThis.window.parent !== globalThis.window) {
|
|
2804
2914
|
window.parent.postMessage({
|
|
2805
2915
|
type: "PLAYCADEMY_DISPLAY_ALERT",
|
|
2806
2916
|
message,
|
|
2807
2917
|
options
|
|
2808
2918
|
}, "*");
|
|
2809
2919
|
} else {
|
|
2810
|
-
const
|
|
2920
|
+
const prefixMap = { error: "❌", warning: "⚠️", info: "ℹ️" };
|
|
2921
|
+
const prefix = (options?.type && prefixMap[options.type]) ?? "ℹ️";
|
|
2811
2922
|
console.log(`${prefix} ${message}`);
|
|
2812
2923
|
}
|
|
2813
2924
|
};
|
|
@@ -2889,18 +3000,16 @@ class PlaycademyBaseClient {
|
|
|
2889
3000
|
authContext;
|
|
2890
3001
|
initPayload;
|
|
2891
3002
|
connectionManager;
|
|
3003
|
+
launchId;
|
|
2892
3004
|
_sessionManager = {
|
|
2893
|
-
startSession: async (gameId) => {
|
|
2894
|
-
|
|
2895
|
-
},
|
|
2896
|
-
endSession: async (sessionId, gameId) => {
|
|
2897
|
-
return this.request(`/games/${gameId}/sessions/${sessionId}`, "DELETE");
|
|
2898
|
-
}
|
|
3005
|
+
startSession: async (gameId) => this.request(`/games/${gameId}/sessions`, "POST"),
|
|
3006
|
+
endSession: async (sessionId, gameId) => this.request(`/games/${gameId}/sessions/${sessionId}`, "DELETE")
|
|
2899
3007
|
};
|
|
2900
3008
|
constructor(config) {
|
|
2901
3009
|
this.baseUrl = config?.baseUrl?.endsWith("/api") ? config.baseUrl : `${config?.baseUrl}/api`;
|
|
2902
3010
|
this.gameUrl = config?.gameUrl;
|
|
2903
3011
|
this.gameId = config?.gameId;
|
|
3012
|
+
this.launchId = config?.launchId ?? undefined;
|
|
2904
3013
|
this.config = config || {};
|
|
2905
3014
|
this.authStrategy = createAuthStrategy(config?.token ?? null, config?.tokenType);
|
|
2906
3015
|
this._detectAuthContext();
|
|
@@ -2909,16 +3018,16 @@ class PlaycademyBaseClient {
|
|
|
2909
3018
|
}
|
|
2910
3019
|
getBaseUrl() {
|
|
2911
3020
|
const isRelative = this.baseUrl.startsWith("/");
|
|
2912
|
-
const isBrowser2 = typeof window !== "undefined";
|
|
2913
|
-
return isRelative && isBrowser2 ? `${
|
|
3021
|
+
const isBrowser2 = typeof globalThis.window !== "undefined";
|
|
3022
|
+
return isRelative && isBrowser2 ? `${globalThis.location.origin}${this.baseUrl}` : this.baseUrl;
|
|
2914
3023
|
}
|
|
2915
3024
|
getGameBackendUrl() {
|
|
2916
3025
|
if (!this.gameUrl) {
|
|
2917
3026
|
throw new PlaycademyError("Game backend URL not configured. gameUrl must be set to use game backend features.");
|
|
2918
3027
|
}
|
|
2919
3028
|
const isRelative = this.gameUrl.startsWith("/");
|
|
2920
|
-
const isBrowser2 = typeof window !== "undefined";
|
|
2921
|
-
const effectiveGameUrl = isRelative && isBrowser2 ? `${
|
|
3029
|
+
const isBrowser2 = typeof globalThis.window !== "undefined";
|
|
3030
|
+
const effectiveGameUrl = isRelative && isBrowser2 ? `${globalThis.location.origin}${this.gameUrl}` : this.gameUrl;
|
|
2922
3031
|
return `${effectiveGameUrl}/api`;
|
|
2923
3032
|
}
|
|
2924
3033
|
ping() {
|
|
@@ -2928,6 +3037,9 @@ class PlaycademyBaseClient {
|
|
|
2928
3037
|
this.authStrategy = createAuthStrategy(token, tokenType);
|
|
2929
3038
|
this.emit("authChange", { token });
|
|
2930
3039
|
}
|
|
3040
|
+
setLaunchId(launchId) {
|
|
3041
|
+
this.launchId = launchId ?? undefined;
|
|
3042
|
+
}
|
|
2931
3043
|
getTokenType() {
|
|
2932
3044
|
return this.authStrategy.getType();
|
|
2933
3045
|
}
|
|
@@ -2950,8 +3062,9 @@ class PlaycademyBaseClient {
|
|
|
2950
3062
|
return this.connectionManager?.getState() ?? "unknown";
|
|
2951
3063
|
}
|
|
2952
3064
|
async checkConnection() {
|
|
2953
|
-
if (!this.connectionManager)
|
|
3065
|
+
if (!this.connectionManager) {
|
|
2954
3066
|
return "unknown";
|
|
3067
|
+
}
|
|
2955
3068
|
return await this.connectionManager.checkNow();
|
|
2956
3069
|
}
|
|
2957
3070
|
_setAuthContext(context) {
|
|
@@ -2969,7 +3082,8 @@ class PlaycademyBaseClient {
|
|
|
2969
3082
|
async request(path, method, options) {
|
|
2970
3083
|
const effectiveHeaders = {
|
|
2971
3084
|
...options?.headers,
|
|
2972
|
-
...this.authStrategy.getHeaders()
|
|
3085
|
+
...this.authStrategy.getHeaders(),
|
|
3086
|
+
...this.launchId ? { "x-playcademy-launch-id": this.launchId } : {}
|
|
2973
3087
|
};
|
|
2974
3088
|
try {
|
|
2975
3089
|
const result = await request({
|
|
@@ -3018,11 +3132,13 @@ class PlaycademyBaseClient {
|
|
|
3018
3132
|
this.authContext = { isInIframe: isInIframe() };
|
|
3019
3133
|
}
|
|
3020
3134
|
_initializeConnectionMonitor() {
|
|
3021
|
-
if (typeof window === "undefined")
|
|
3135
|
+
if (typeof globalThis.window === "undefined") {
|
|
3022
3136
|
return;
|
|
3137
|
+
}
|
|
3023
3138
|
const isEnabled = this.config.enableConnectionMonitoring ?? true;
|
|
3024
|
-
if (!isEnabled)
|
|
3139
|
+
if (!isEnabled) {
|
|
3025
3140
|
return;
|
|
3141
|
+
}
|
|
3026
3142
|
try {
|
|
3027
3143
|
this.connectionManager = new ConnectionManager({
|
|
3028
3144
|
baseUrl: this.baseUrl,
|
|
@@ -3037,11 +3153,13 @@ class PlaycademyBaseClient {
|
|
|
3037
3153
|
}
|
|
3038
3154
|
}
|
|
3039
3155
|
async _initializeInternalSession() {
|
|
3040
|
-
if (!this.gameId || this.internalClientSessionId)
|
|
3156
|
+
if (!this.gameId || this.internalClientSessionId) {
|
|
3041
3157
|
return;
|
|
3158
|
+
}
|
|
3042
3159
|
const shouldAutoStart = this.config.autoStartSession ?? true;
|
|
3043
|
-
if (!shouldAutoStart)
|
|
3160
|
+
if (!shouldAutoStart) {
|
|
3044
3161
|
return;
|
|
3162
|
+
}
|
|
3045
3163
|
try {
|
|
3046
3164
|
const response = await this._sessionManager.startSession(this.gameId);
|
|
3047
3165
|
this.internalClientSessionId = response.sessionId;
|
|
@@ -3092,6 +3210,7 @@ export {
|
|
|
3092
3210
|
PlaycademyError,
|
|
3093
3211
|
PlaycademyInternalClient as PlaycademyClient,
|
|
3094
3212
|
MessageEvents,
|
|
3213
|
+
ManifestError,
|
|
3095
3214
|
ConnectionMonitor,
|
|
3096
3215
|
ConnectionManager,
|
|
3097
3216
|
ApiError
|