@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/internal.js CHANGED
@@ -32,15 +32,15 @@ class PlaycademyMessaging {
32
32
  }
33
33
  }
34
34
  listen(type, handler) {
35
- const postMessageListener = (event) => {
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
- const customEventListener = (event) => {
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
- window.addEventListener(type, customEventListener);
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
- window.removeEventListener(type, listeners.customEvent);
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" && window.self !== window.top;
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
- window.dispatchEvent(new CustomEvent(type, { detail: payload }));
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 = window.PLAYCADEMY;
99
+ const preloaded = globalThis.PLAYCADEMY;
100
100
  if (preloaded?.token) {
101
101
  return preloaded;
102
102
  }
103
- if (window.self !== window.top) {
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 (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1") {
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
- const handleMessage = (event) => {
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
- window.PLAYCADEMY = event.data.payload;
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: window.location.origin,
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
- window.PLAYCADEMY = mockConfig;
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: window.self !== window.top,
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
- var isBrowser = () => {
204
+ function isBrowser() {
202
205
  const g = globalThis;
203
206
  return typeof g.window !== "undefined" && typeof g.document !== "undefined";
204
- };
205
- var isProduction = () => {
207
+ }
208
+ function isProduction() {
206
209
  return typeof process !== "undefined" && false;
207
- };
208
- var isDevelopment = () => {
210
+ }
211
+ function isDevelopment() {
209
212
  return typeof process !== "undefined" && true;
210
- };
211
- var isInteractiveTTY = () => {
213
+ }
214
+ function isInteractiveTTY() {
212
215
  return typeof process !== "undefined" && Boolean(process.stdout && process.stdout.isTTY);
213
- };
214
- var isSilent = () => {
216
+ }
217
+ function isSilent() {
215
218
  return typeof process !== "undefined" && process.env.LOG_SILENT === "true";
216
- };
217
- var detectOutputFormat = () => {
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
- var getLevelColor = (level) => {
255
+ function getLevelColor(level) {
253
256
  switch (level) {
254
- case "debug":
257
+ case "debug": {
255
258
  return colors.blue;
256
- case "info":
259
+ }
260
+ case "info": {
257
261
  return colors.cyan;
258
- case "warn":
262
+ }
263
+ case "warn": {
259
264
  return colors.yellow;
260
- case "error":
265
+ }
266
+ case "error": {
261
267
  return colors.red;
262
- default:
268
+ }
269
+ default: {
263
270
  return colors.reset;
271
+ }
264
272
  }
265
- };
266
- var formatBrowserOutput = (level, message, context, scope) => {
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
- var formatColorTTY = (level, message, context, scope) => {
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
- var formatJSONSingleLine = (level, message, context, scope) => {
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
- var formatJSONPretty = (level, message, context, scope) => {
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
- var getConsoleMethod = (level) => {
321
+ }
322
+ function getConsoleMethod(level) {
315
323
  switch (level) {
316
- case "debug":
324
+ case "debug": {
317
325
  return console.debug;
318
- case "info":
326
+ }
327
+ case "info": {
319
328
  return console.info;
320
- case "warn":
329
+ }
330
+ case "warn": {
321
331
  return console.warn;
322
- case "error":
332
+ }
333
+ case "error": {
323
334
  return console.error;
324
- default:
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
- var getMinimumLogLevel = () => {
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
- var shouldLog = (level) => {
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
- var performLog = (level, message, context, scope) => {
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
- case "color-tty":
375
+ }
376
+ case "color-tty": {
361
377
  formatColorTTY(level, message, context, scope);
362
378
  break;
363
- case "json-single-line":
379
+ }
380
+ case "json-single-line": {
364
381
  formatJSONSingleLine(level, message, context, scope);
365
382
  break;
366
- case "json-pretty":
383
+ }
384
+ case "json-pretty": {
367
385
  formatJSONPretty(level, message, context, scope);
368
386
  break;
387
+ }
369
388
  }
370
- };
371
- var createLogger = (scopeName) => {
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
- case 401:
461
+ }
462
+ case 401: {
433
463
  return "UNAUTHORIZED";
434
- case 403:
464
+ }
465
+ case 403: {
435
466
  return "FORBIDDEN";
436
- case 404:
467
+ }
468
+ case 404: {
437
469
  return "NOT_FOUND";
438
- case 405:
470
+ }
471
+ case 405: {
439
472
  return "METHOD_NOT_ALLOWED";
440
- case 409:
473
+ }
474
+ case 409: {
441
475
  return "CONFLICT";
442
- case 410:
476
+ }
477
+ case 410: {
443
478
  return "GONE";
444
- case 412:
479
+ }
480
+ case 412: {
445
481
  return "PRECONDITION_FAILED";
446
- case 413:
482
+ }
483
+ case 413: {
447
484
  return "PAYLOAD_TOO_LARGE";
448
- case 422:
485
+ }
486
+ case 422: {
449
487
  return "VALIDATION_FAILED";
450
- case 429:
488
+ }
489
+ case 429: {
451
490
  return "TOO_MANY_REQUESTS";
452
- case 500:
491
+ }
492
+ case 500: {
453
493
  return "INTERNAL_ERROR";
454
- case 501:
494
+ }
495
+ case 501: {
455
496
  return "NOT_IMPLEMENTED";
456
- case 503:
497
+ }
498
+ case 503: {
457
499
  return "SERVICE_UNAVAILABLE";
458
- case 504:
500
+ }
501
+ case 504: {
459
502
  return "TIMEOUT";
460
- default:
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 = window.location.origin + baseUrl;
524
+ if (baseUrl.startsWith("/") && typeof globalThis.window !== "undefined") {
525
+ url = globalThis.location.origin + baseUrl;
481
526
  }
482
- url = url + "/auth/login";
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 Array.from(randomValues).map((byte) => charset[byte % charset.length]).join("");
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 window.self !== window.top;
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
- const handleMessage = (event) => {
635
- if (event.origin !== window.location.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
- window.location.href = authUrl;
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
- const effectiveMode = mode === "auto" ? isInIframe() ? "popup" : "redirect" : mode;
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
- case "redirect":
797
+ }
798
+ case "redirect": {
746
799
  return initiateRedirectFlow(options);
747
- default:
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: !!stateData
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
- const trackListener = (eventType, handler) => {
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
- const untrackListener = (eventType, handler) => {
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" && window.self !== window.top) {
821
- const playcademyConfig = window.PLAYCADEMY;
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
- window.addEventListener("keydown", keyListener);
834
- window.addEventListener("keyup", keyListener);
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
- window.removeEventListener("keydown", keyListener);
837
- window.removeEventListener("keyup", keyListener);
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
- const fetchAsset = async (path, options) => {
969
+ async function fetchAsset(path, options) {
915
970
  const gameUrl = client["initPayload"]?.gameUrl;
916
971
  if (!gameUrl) {
917
- const relativePath = path.startsWith("./") ? path : "./" + 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 + cleanPath, options);
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 : "./" + path;
989
+ return path.startsWith("./") ? path : `./${path}`;
937
990
  }
938
991
  const cleanPath = path.startsWith("./") ? path.slice(2) : path;
939
- return gameUrl + cleanPath;
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
- const resolveItemId = async (identifier) => {
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 TSC_PACKAGE = "typescript";
1189
- var USE_NATIVE_TSC = TSC_PACKAGE.includes("native-preview");
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
- const getCreditsItemId = async () => {
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
- return client["request"](`/games/${gameId}/scores`, "POST", {
1303
- body: {
1304
- score,
1305
- metadata
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
- const getTimeback = () => client["initPayload"]?.timeback;
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
- return userCache.get("current", async () => {
1443
- const response = await client["request"]("/timeback/user", "GET");
1444
- const initPayload = client["initPayload"];
1445
- if (initPayload) {
1446
- initPayload.timeback = response;
1447
- }
1448
- return {
1449
- id: response.id,
1450
- role: response.role,
1451
- enrollments: response.enrollments,
1452
- organizations: response.organizations
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
- return client["request"]("/dev/api-keys", "POST", {
1595
- body: {
1596
- name: options?.name || `SDK Key - ${new Date().toISOString()}`,
1597
- expiresIn: options?.expiresIn !== undefined ? options.expiresIn : null,
1598
- permissions: options?.permissions || {
1599
- games: ["read", "write", "delete"],
1600
- users: ["read:self", "write:self"],
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/namespaces/platform/dev.ts
1652
- function createDevNamespace(client) {
1653
- const DEPLOY_JOB_POLL_INTERVAL_MS = 1000;
1654
- const DEPLOY_JOB_INACTIVITY_TIMEOUT_MS = 60 * 1000;
1655
- async function pollDeployJob(slug, jobId, hooks) {
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 > DEPLOY_JOB_INACTIVITY_TIMEOUT_MS) {
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, DEPLOY_JOB_POLL_INTERVAL_MS));
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
- let uploadToken;
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
- const requestBody = {};
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
- return client["request"](`/games/${slug}/seed`, "POST", {
1796
- body: { code, environment, secrets }
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
- return client["request"](`/games/${slug}/database/reset`, "POST", {
1817
- body: { schema }
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
- return client["request"](`/games/${slug}/domains`, "POST", {
1886
- body: { hostname }
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
- return client["request"](`/games/${gameId}/items/${itemId}/shop-listing`, "POST", { body: listingData });
1926
- },
1927
- get: (gameId, itemId) => {
1928
- return client["request"](`/games/${gameId}/items/${itemId}/shop-listing`, "GET");
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
- function checkDevWarnings(data) {
1945
- if (!data || typeof data !== "object")
1946
- return;
1947
- const response = data;
1948
- const warningType = response.__playcademyDevWarning;
1949
- if (!warningType)
1950
- return;
1951
- switch (warningType) {
1952
- case "timeback-not-configured":
1953
- console.warn("%c⚠️ TimeBack Not Configured", "background: #f59e0b; color: white; padding: 6px 12px; border-radius: 4px; font-weight: bold; font-size: 13px");
1954
- console.log("%cTimeBack is configured in playcademy.config.js but the sandbox does not have TimeBack credentials.", "color: #f59e0b; font-weight: 500");
1955
- console.log("To test TimeBack locally:");
1956
- console.log(" Set the following environment variables:");
1957
- console.log(" • %cTIMEBACK_ONEROSTER_API_URL", "color: #0ea5e9; font-weight: 600; font-family: monospace");
1958
- console.log(" • %cTIMEBACK_CALIPER_API_URL", "color: #0ea5e9; font-weight: 600; font-family: monospace");
1959
- console.log(" • %cTIMEBACK_API_CLIENT_ID/SECRET", "color: #0ea5e9; font-weight: 600; font-family: monospace");
1960
- console.log(" Or deploy your game: %cplaycademy deploy", "color: #10b981; font-weight: 600; font-family: monospace");
1961
- console.log(" Or wait for %c@superbuilders/timeback-local%c (coming soon)", "color: #8b5cf6; font-weight: 600; font-family: monospace", "color: inherit");
1962
- break;
1963
- default:
1964
- console.warn(`[Playcademy Dev Warning] ${warningType}`);
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 fetch(url, {
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 (err) {
2022
- if (err instanceof SyntaxError)
2151
+ } catch (error) {
2152
+ if (error instanceof SyntaxError) {
2023
2153
  return;
2024
- throw err;
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
- if (error instanceof PlaycademyError) {
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 PlaycademyError("Failed to load or parse game manifest");
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
- return gamesListCache.get("all", () => client["request"]("/games", "GET"), options);
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
- if (options?.offset)
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
- return client["request"]("/character", "POST", {
2134
- body: characterData
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
- return client["request"]("/character/accessories/equip", "POST", { body: { slot, accessoryComponentId: componentId } });
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
- return client["request"]("/users/level", "GET");
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
- return client["request"]("/levels/config", "GET");
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
- if (queryOptions?.type)
2420
+ }
2421
+ if (queryOptions?.type) {
2308
2422
  params.append("type", queryOptions.type);
2309
- if (queryOptions?.limit)
2423
+ }
2424
+ if (queryOptions?.limit) {
2310
2425
  params.append("limit", String(queryOptions.limit));
2311
- if (queryOptions?.offset)
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
- if (queryOptions?.to)
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
- const templateMeta = await templateUrlCache.get(slug, async () => {
2411
- return client["request"](`/sprites/templates/${slug}`, "GET");
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
- const getTimeback = () => client["initPayload"]?.timeback;
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
- return userCache.get("current", async () => {
2480
- const response = await client["request"]("/timeback/user", "GET");
2481
- const initPayload = client["initPayload"];
2482
- if (initPayload) {
2483
- initPayload.timeback = response;
2484
- }
2485
- return {
2486
- id: response.id,
2487
- role: response.role,
2488
- enrollments: response.enrollments,
2489
- organizations: response.organizations
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
- return client["request"]("/timeback/setup", "POST", {
2513
- body: request2
2514
- });
2515
- },
2516
- verify: (gameId) => {
2517
- return client["request"](`/timeback/verify/${gameId}`, "GET");
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
- if (options?.timezone)
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
- if (options?.endDate)
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
- if (options?.timezone)
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
- window.addEventListener("online", this._handleOnline);
2685
- window.addEventListener("offline", this._handleOffline);
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
- window.removeEventListener("online", this._handleOnline);
2697
- window.removeEventListener("offline", this._handleOffline);
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 prefix = options?.type === "error" ? "❌" : options?.type === "warning" ? "⚠️" : "ℹ️";
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
- return this.request(`/games/${gameId}/sessions`, "POST");
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 ? `${window.location.origin}${this.baseUrl}` : this.baseUrl;
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 ? `${window.location.origin}${this.gameUrl}` : this.gameUrl;
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