@playcademy/sdk 0.1.17 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,28 +1,21 @@
1
- var __defProp = Object.defineProperty;
2
- var __export = (target, all) => {
3
- for (var name in all)
4
- __defProp(target, name, {
5
- get: all[name],
6
- enumerable: true,
7
- configurable: true,
8
- set: (newValue) => all[name] = () => newValue
9
- });
10
- };
11
- var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
12
-
13
1
  // ../logger/src/index.ts
14
2
  var isBrowser = () => {
15
3
  const g = globalThis;
16
4
  return typeof g.window !== "undefined" && typeof g.document !== "undefined";
17
- }, isProduction = () => {
5
+ };
6
+ var isProduction = () => {
18
7
  return typeof process !== "undefined" && false;
19
- }, isDevelopment = () => {
8
+ };
9
+ var isDevelopment = () => {
20
10
  return typeof process !== "undefined" && true;
21
- }, isInteractiveTTY = () => {
11
+ };
12
+ var isInteractiveTTY = () => {
22
13
  return typeof process !== "undefined" && Boolean(process.stdout && process.stdout.isTTY);
23
- }, isSilent = () => {
14
+ };
15
+ var isSilent = () => {
24
16
  return typeof process !== "undefined" && process.env.LOG_SILENT === "true";
25
- }, detectOutputFormat = () => {
17
+ };
18
+ var detectOutputFormat = () => {
26
19
  if (isBrowser()) {
27
20
  return "browser";
28
21
  }
@@ -46,7 +39,17 @@ var isBrowser = () => {
46
39
  return "color-tty";
47
40
  }
48
41
  return "json-single-line";
49
- }, colors, getLevelColor = (level) => {
42
+ };
43
+ var colors = {
44
+ reset: "\x1B[0m",
45
+ dim: "\x1B[2m",
46
+ red: "\x1B[31m",
47
+ yellow: "\x1B[33m",
48
+ blue: "\x1B[34m",
49
+ cyan: "\x1B[36m",
50
+ gray: "\x1B[90m"
51
+ };
52
+ var getLevelColor = (level) => {
50
53
  switch (level) {
51
54
  case "debug":
52
55
  return colors.blue;
@@ -59,7 +62,8 @@ var isBrowser = () => {
59
62
  default:
60
63
  return colors.reset;
61
64
  }
62
- }, formatBrowserOutput = (level, message, context) => {
65
+ };
66
+ var formatBrowserOutput = (level, message, context) => {
63
67
  const timestamp = new Date().toISOString();
64
68
  const levelUpper = level.toUpperCase();
65
69
  const consoleMethod = getConsoleMethod(level);
@@ -68,7 +72,8 @@ var isBrowser = () => {
68
72
  } else {
69
73
  consoleMethod(`[${timestamp}] ${levelUpper}`, message);
70
74
  }
71
- }, formatColorTTY = (level, message, context) => {
75
+ };
76
+ var formatColorTTY = (level, message, context) => {
72
77
  const timestamp = new Date().toISOString();
73
78
  const levelColor = getLevelColor(level);
74
79
  const levelUpper = level.toUpperCase().padEnd(5);
@@ -79,7 +84,8 @@ var isBrowser = () => {
79
84
  } else {
80
85
  consoleMethod(`${coloredPrefix} ${message}`);
81
86
  }
82
- }, formatJSONSingleLine = (level, message, context) => {
87
+ };
88
+ var formatJSONSingleLine = (level, message, context) => {
83
89
  const timestamp = new Date().toISOString();
84
90
  const logEntry = {
85
91
  timestamp,
@@ -89,7 +95,8 @@ var isBrowser = () => {
89
95
  };
90
96
  const consoleMethod = getConsoleMethod(level);
91
97
  consoleMethod(JSON.stringify(logEntry));
92
- }, formatJSONPretty = (level, message, context) => {
98
+ };
99
+ var formatJSONPretty = (level, message, context) => {
93
100
  const timestamp = new Date().toISOString();
94
101
  const logEntry = {
95
102
  timestamp,
@@ -99,7 +106,8 @@ var isBrowser = () => {
99
106
  };
100
107
  const consoleMethod = getConsoleMethod(level);
101
108
  consoleMethod(JSON.stringify(logEntry, null, 2));
102
- }, getConsoleMethod = (level) => {
109
+ };
110
+ var getConsoleMethod = (level) => {
103
111
  switch (level) {
104
112
  case "debug":
105
113
  return console.debug;
@@ -112,18 +120,28 @@ var isBrowser = () => {
112
120
  default:
113
121
  return console.log;
114
122
  }
115
- }, levelPriority, getMinimumLogLevel = () => {
123
+ };
124
+ var levelPriority = {
125
+ debug: 0,
126
+ info: 1,
127
+ warn: 2,
128
+ error: 3
129
+ };
130
+ var getMinimumLogLevel = () => {
116
131
  const envLevel = typeof process !== "undefined" ? (process.env.LOG_LEVEL ?? "").toLowerCase() : "";
117
132
  if (envLevel && ["debug", "info", "warn", "error"].includes(envLevel)) {
118
133
  return envLevel;
119
134
  }
120
135
  return isProduction() ? "info" : "debug";
121
- }, shouldLog = (level) => {
136
+ };
137
+ var shouldLog = (level) => {
122
138
  if (isSilent())
123
139
  return false;
124
140
  const minLevel = getMinimumLogLevel();
125
141
  return levelPriority[level] >= levelPriority[minLevel];
126
- }, customHandler, performLog = (level, message, context) => {
142
+ };
143
+ var customHandler;
144
+ var performLog = (level, message, context) => {
127
145
  if (!shouldLog(level))
128
146
  return;
129
147
  if (customHandler) {
@@ -145,7 +163,8 @@ var isBrowser = () => {
145
163
  formatJSONPretty(level, message, context);
146
164
  break;
147
165
  }
148
- }, createLogger = () => {
166
+ };
167
+ var createLogger = () => {
149
168
  return {
150
169
  debug: (message, context) => performLog("debug", message, context),
151
170
  info: (message, context) => performLog("info", message, context),
@@ -153,25 +172,8 @@ var isBrowser = () => {
153
172
  error: (message, context) => performLog("error", message, context),
154
173
  log: performLog
155
174
  };
156
- }, log;
157
- var init_src = __esm(() => {
158
- colors = {
159
- reset: "\x1B[0m",
160
- dim: "\x1B[2m",
161
- red: "\x1B[31m",
162
- yellow: "\x1B[33m",
163
- blue: "\x1B[34m",
164
- cyan: "\x1B[36m",
165
- gray: "\x1B[90m"
166
- };
167
- levelPriority = {
168
- debug: 0,
169
- info: 1,
170
- warn: 2,
171
- error: 3
172
- };
173
- log = createLogger();
174
- });
175
+ };
176
+ var log = createLogger();
175
177
 
176
178
  // src/core/auth/strategies.ts
177
179
  class ApiKeyAuth {
@@ -421,8 +423,25 @@ class ConnectionMonitor {
421
423
  });
422
424
  }
423
425
  }
424
-
425
426
  // src/messaging.ts
427
+ var MessageEvents;
428
+ ((MessageEvents2) => {
429
+ MessageEvents2["INIT"] = "PLAYCADEMY_INIT";
430
+ MessageEvents2["TOKEN_REFRESH"] = "PLAYCADEMY_TOKEN_REFRESH";
431
+ MessageEvents2["PAUSE"] = "PLAYCADEMY_PAUSE";
432
+ MessageEvents2["RESUME"] = "PLAYCADEMY_RESUME";
433
+ MessageEvents2["FORCE_EXIT"] = "PLAYCADEMY_FORCE_EXIT";
434
+ MessageEvents2["OVERLAY"] = "PLAYCADEMY_OVERLAY";
435
+ MessageEvents2["CONNECTION_STATE"] = "PLAYCADEMY_CONNECTION_STATE";
436
+ MessageEvents2["READY"] = "PLAYCADEMY_READY";
437
+ MessageEvents2["EXIT"] = "PLAYCADEMY_EXIT";
438
+ MessageEvents2["TELEMETRY"] = "PLAYCADEMY_TELEMETRY";
439
+ MessageEvents2["KEY_EVENT"] = "PLAYCADEMY_KEY_EVENT";
440
+ MessageEvents2["DISPLAY_ALERT"] = "PLAYCADEMY_DISPLAY_ALERT";
441
+ MessageEvents2["AUTH_STATE_CHANGE"] = "PLAYCADEMY_AUTH_STATE_CHANGE";
442
+ MessageEvents2["AUTH_CALLBACK"] = "PLAYCADEMY_AUTH_CALLBACK";
443
+ })(MessageEvents ||= {});
444
+
426
445
  class PlaycademyMessaging {
427
446
  listeners = new Map;
428
447
  send(type, payload, options) {
@@ -498,26 +517,7 @@ class PlaycademyMessaging {
498
517
  window.dispatchEvent(new CustomEvent(type, { detail: payload }));
499
518
  }
500
519
  }
501
- var MessageEvents, messaging;
502
- var init_messaging = __esm(() => {
503
- ((MessageEvents2) => {
504
- MessageEvents2["INIT"] = "PLAYCADEMY_INIT";
505
- MessageEvents2["TOKEN_REFRESH"] = "PLAYCADEMY_TOKEN_REFRESH";
506
- MessageEvents2["PAUSE"] = "PLAYCADEMY_PAUSE";
507
- MessageEvents2["RESUME"] = "PLAYCADEMY_RESUME";
508
- MessageEvents2["FORCE_EXIT"] = "PLAYCADEMY_FORCE_EXIT";
509
- MessageEvents2["OVERLAY"] = "PLAYCADEMY_OVERLAY";
510
- MessageEvents2["CONNECTION_STATE"] = "PLAYCADEMY_CONNECTION_STATE";
511
- MessageEvents2["READY"] = "PLAYCADEMY_READY";
512
- MessageEvents2["EXIT"] = "PLAYCADEMY_EXIT";
513
- MessageEvents2["TELEMETRY"] = "PLAYCADEMY_TELEMETRY";
514
- MessageEvents2["KEY_EVENT"] = "PLAYCADEMY_KEY_EVENT";
515
- MessageEvents2["DISPLAY_ALERT"] = "PLAYCADEMY_DISPLAY_ALERT";
516
- MessageEvents2["AUTH_STATE_CHANGE"] = "PLAYCADEMY_AUTH_STATE_CHANGE";
517
- MessageEvents2["AUTH_CALLBACK"] = "PLAYCADEMY_AUTH_CALLBACK";
518
- })(MessageEvents ||= {});
519
- messaging = new PlaycademyMessaging;
520
- });
520
+ var messaging = new PlaycademyMessaging;
521
521
 
522
522
  // src/core/connection/utils.ts
523
523
  function createDisplayAlert(authContext) {
@@ -599,16 +599,24 @@ class ConnectionManager {
599
599
  }
600
600
  }
601
601
  }
602
- var init_manager = __esm(() => {
603
- init_messaging();
604
- });
605
-
606
- // src/core/connection/index.ts
607
- var init_connection = __esm(() => {
608
- init_manager();
609
- });
610
-
611
602
  // src/core/errors.ts
603
+ class PlaycademyError extends Error {
604
+ constructor(message) {
605
+ super(message);
606
+ this.name = "PlaycademyError";
607
+ }
608
+ }
609
+
610
+ class ApiError extends Error {
611
+ status;
612
+ details;
613
+ constructor(status, message, details) {
614
+ super(`${status} ${message}`);
615
+ this.status = status;
616
+ this.details = details;
617
+ Object.setPrototypeOf(this, ApiError.prototype);
618
+ }
619
+ }
612
620
  function extractApiErrorInfo(error) {
613
621
  if (!(error instanceof ApiError)) {
614
622
  return null;
@@ -617,216 +625,370 @@ function extractApiErrorInfo(error) {
617
625
  status: error.status,
618
626
  statusText: error.message
619
627
  };
620
- if (error.details && typeof error.details === "object") {
621
- const details = error.details;
622
- if ("error" in details && typeof details.error === "string") {
623
- info.error = details.error;
624
- }
625
- if ("message" in details && typeof details.message === "string") {
626
- info.message = details.message;
627
- }
628
- if (!info.error && !info.message) {
629
- info.details = error.details;
630
- }
628
+ if (error.details) {
629
+ info.details = error.details;
631
630
  }
632
631
  return info;
633
632
  }
634
- var PlaycademyError, ApiError;
635
- var init_errors = __esm(() => {
636
- PlaycademyError = class PlaycademyError extends Error {
637
- constructor(message) {
638
- super(message);
639
- this.name = "PlaycademyError";
640
- }
641
- };
642
- ApiError = class ApiError extends Error {
643
- status;
644
- details;
645
- constructor(status, message, details) {
646
- super(`${status} ${message}`);
647
- this.status = status;
648
- this.details = details;
649
- Object.setPrototypeOf(this, ApiError.prototype);
650
- }
651
- };
652
- });
653
-
654
- // src/core/namespaces/auth.ts
655
- function createAuthNamespace(client) {
656
- return {
657
- login: async (credentials) => {
658
- try {
659
- const response = await client["request"]("/auth/sign-in/email", "POST", { body: credentials });
660
- client.setToken(response.token, "session");
661
- return {
662
- success: true,
663
- token: response.token,
664
- user: response.user,
665
- expiresAt: response.expiresAt
666
- };
667
- } catch (error) {
668
- return {
669
- success: false,
670
- error: error instanceof Error ? error.message : "Authentication failed"
671
- };
672
- }
673
- },
674
- logout: async () => {
675
- try {
676
- await client["request"]("/auth/sign-out", "POST");
677
- } catch {}
678
- client.setToken(null);
679
- },
680
- apiKeys: {
681
- create: async (options) => {
682
- return client["request"]("/dev/api-keys", "POST", {
683
- body: {
684
- name: options?.name || `SDK Key - ${new Date().toISOString()}`,
685
- expiresIn: options?.expiresIn !== undefined ? options.expiresIn : null,
686
- permissions: options?.permissions || {
687
- games: ["read", "write", "delete"],
688
- users: ["read:self", "write:self"],
689
- dev: ["read", "write"]
690
- }
691
- }
692
- });
693
- },
694
- list: async () => {
695
- return client["request"]("/auth/api-key/list", "GET");
696
- },
697
- revoke: async (keyId) => {
698
- await client["request"]("/auth/api-key/revoke", "POST", {
699
- body: { id: keyId }
700
- });
701
- }
702
- }
703
- };
704
- }
705
-
706
- // ../utils/src/random.ts
707
- async function generateSecureRandomString(length) {
708
- const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
709
- const randomValues = new Uint8Array(length);
710
- globalThis.crypto.getRandomValues(randomValues);
711
- return Array.from(randomValues).map((byte) => charset[byte % charset.length]).join("");
712
- }
713
633
 
714
- // src/core/auth/oauth.ts
715
- function getTimebackConfig() {
716
- return {
717
- authorizationEndpoint: "https://alpha-auth-production-idp.auth.us-west-2.amazoncognito.com/oauth2/authorize",
718
- tokenEndpoint: "https://alpha-auth-production-idp.auth.us-west-2.amazoncognito.com/oauth2/token",
719
- scope: "openid email phone"
720
- };
634
+ // src/core/request.ts
635
+ function checkDevWarnings(data) {
636
+ if (!data || typeof data !== "object")
637
+ return;
638
+ const response = data;
639
+ const warningType = response.__playcademyDevWarning;
640
+ if (!warningType)
641
+ return;
642
+ switch (warningType) {
643
+ case "timeback-not-configured":
644
+ console.warn("%c⚠️ TimeBack Not Configured", "background: #f59e0b; color: white; padding: 6px 12px; border-radius: 4px; font-weight: bold; font-size: 13px");
645
+ console.log("%cTimeBack is configured in playcademy.config.js but the sandbox does not have TimeBack credentials.", "color: #f59e0b; font-weight: 500");
646
+ console.log("To test TimeBack locally:");
647
+ console.log(" Set the following environment variables:");
648
+ console.log(" • %cTIMEBACK_ONEROSTER_API_URL", "color: #0ea5e9; font-weight: 600; font-family: monospace");
649
+ console.log(" • %cTIMEBACK_CALIPER_API_URL", "color: #0ea5e9; font-weight: 600; font-family: monospace");
650
+ console.log(" • %cTIMEBACK_API_CLIENT_ID/SECRET", "color: #0ea5e9; font-weight: 600; font-family: monospace");
651
+ console.log(" Or deploy your game: %cplaycademy deploy", "color: #10b981; font-weight: 600; font-family: monospace");
652
+ console.log(" Or wait for %c@superbuilders/timeback-local%c (coming soon)", "color: #8b5cf6; font-weight: 600; font-family: monospace", "color: inherit");
653
+ break;
654
+ default:
655
+ console.warn(`[Playcademy Dev Warning] ${warningType}`);
656
+ }
721
657
  }
722
- async function generateOAuthState(data) {
723
- const csrfToken = await generateSecureRandomString(32);
724
- if (data && Object.keys(data).length > 0) {
725
- const jsonStr = JSON.stringify(data);
726
- const base64 = btoa(jsonStr).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
727
- return `${csrfToken}.${base64}`;
658
+ function prepareRequestBody(body, headers) {
659
+ if (body instanceof FormData) {
660
+ return body;
728
661
  }
729
- return csrfToken;
662
+ if (body instanceof ArrayBuffer || body instanceof Blob || ArrayBuffer.isView(body)) {
663
+ if (!headers["Content-Type"]) {
664
+ headers["Content-Type"] = "application/octet-stream";
665
+ }
666
+ return body;
667
+ }
668
+ if (body !== undefined && body !== null) {
669
+ headers["Content-Type"] = "application/json";
670
+ return JSON.stringify(body);
671
+ }
672
+ return;
730
673
  }
731
- function parseOAuthState(state) {
732
- const lastDotIndex = state.lastIndexOf(".");
733
- if (lastDotIndex > 0 && lastDotIndex < state.length - 1) {
674
+ async function request({
675
+ path,
676
+ baseUrl,
677
+ method = "GET",
678
+ body,
679
+ extraHeaders = {},
680
+ raw = false
681
+ }) {
682
+ const url = baseUrl.replace(/\/$/, "") + (path.startsWith("/") ? path : `/${path}`);
683
+ const headers = { ...extraHeaders };
684
+ const payload = prepareRequestBody(body, headers);
685
+ const res = await fetch(url, {
686
+ method,
687
+ headers,
688
+ body: payload,
689
+ credentials: "omit"
690
+ });
691
+ if (raw) {
692
+ return res;
693
+ }
694
+ if (!res.ok) {
695
+ const clonedRes = res.clone();
696
+ const errorBody = await clonedRes.json().catch(() => clonedRes.text().catch(() => {
697
+ return;
698
+ })) ?? undefined;
699
+ throw new ApiError(res.status, res.statusText, errorBody);
700
+ }
701
+ if (res.status === 204)
702
+ return;
703
+ const contentType = res.headers.get("content-type") ?? "";
704
+ if (contentType.includes("application/json")) {
734
705
  try {
735
- const csrfToken = state.substring(0, lastDotIndex);
736
- const base64 = state.substring(lastDotIndex + 1);
737
- const base64WithPadding = base64.replace(/-/g, "+").replace(/_/g, "/");
738
- const paddedBase64 = base64WithPadding + "=".repeat((4 - base64WithPadding.length % 4) % 4);
739
- const jsonStr = atob(paddedBase64);
740
- const data = JSON.parse(jsonStr);
741
- return { csrfToken, data };
742
- } catch {}
706
+ const parsed = await res.json();
707
+ checkDevWarnings(parsed);
708
+ return parsed;
709
+ } catch (err) {
710
+ if (err instanceof SyntaxError)
711
+ return;
712
+ throw err;
713
+ }
743
714
  }
744
- return { csrfToken: state };
745
- }
746
- function getOAuthConfig(provider) {
747
- const configGetter = OAUTH_CONFIGS[provider];
748
- if (!configGetter)
749
- throw new Error(`Unsupported auth provider: ${provider}`);
750
- return configGetter();
715
+ const rawText = await res.text().catch(() => "");
716
+ return rawText && rawText.length > 0 ? rawText : undefined;
751
717
  }
752
- var OAUTH_CONFIGS;
753
- var init_oauth = __esm(() => {
754
- OAUTH_CONFIGS = {
755
- get TIMEBACK() {
756
- return getTimebackConfig;
757
- }
758
- };
759
- });
760
-
761
- // src/core/auth/flows/popup.ts
762
- async function initiatePopupFlow(options) {
763
- const { provider, callbackUrl, onStateChange, oauth } = options;
718
+ async function fetchManifest(deploymentUrl) {
719
+ const manifestUrl = `${deploymentUrl.replace(/\/$/, "")}/playcademy.manifest.json`;
764
720
  try {
765
- onStateChange?.({
766
- status: "opening_popup",
767
- message: "Opening authentication window..."
768
- });
769
- const defaults = getOAuthConfig(provider);
770
- const config = oauth ? { ...defaults, ...oauth } : defaults;
771
- if (!config.clientId) {
772
- throw new Error(`clientId is required for ${provider} authentication. ` + "Please provide it in the oauth parameter.");
773
- }
774
- const stateData = options.stateData;
775
- const state = await generateOAuthState(stateData);
776
- const params = new URLSearchParams({
777
- response_type: "code",
778
- client_id: config.clientId,
779
- redirect_uri: callbackUrl,
780
- state
781
- });
782
- if (config.scope) {
783
- params.set("scope", config.scope);
784
- }
785
- const authUrl = `${config.authorizationEndpoint}?${params.toString()}`;
786
- const popup = openPopupWindow(authUrl, "playcademy-auth");
787
- if (!popup || popup.closed) {
788
- throw new Error("Popup blocked. Please enable popups and try again.");
721
+ const response = await fetch(manifestUrl);
722
+ if (!response.ok) {
723
+ log.error(`[fetchManifest] Failed to fetch manifest from ${manifestUrl}. Status: ${response.status}`);
724
+ throw new PlaycademyError(`Failed to fetch manifest: ${response.status} ${response.statusText}`);
789
725
  }
790
- onStateChange?.({
791
- status: "exchanging_token",
792
- message: "Waiting for authentication..."
793
- });
794
- return await waitForServerMessage(popup, onStateChange);
726
+ return await response.json();
795
727
  } catch (error) {
796
- const errorMessage = error instanceof Error ? error.message : "Authentication failed";
797
- onStateChange?.({
798
- status: "error",
799
- message: errorMessage,
800
- error: error instanceof Error ? error : new Error(errorMessage)
728
+ if (error instanceof PlaycademyError) {
729
+ throw error;
730
+ }
731
+ log.error(`[Playcademy SDK] Error fetching or parsing manifest from ${manifestUrl}:`, {
732
+ error
801
733
  });
802
- throw error;
734
+ throw new PlaycademyError("Failed to load or parse game manifest");
803
735
  }
804
736
  }
805
- async function waitForServerMessage(popup, onStateChange) {
806
- return new Promise((resolve) => {
807
- let resolved = false;
808
- const handleMessage = (event) => {
809
- if (event.origin !== window.location.origin)
810
- return;
811
- const data = event.data;
812
- if (data?.type === "PLAYCADEMY_AUTH_STATE_CHANGE") {
813
- resolved = true;
814
- window.removeEventListener("message", handleMessage);
815
- if (data.authenticated && data.user) {
816
- onStateChange?.({
817
- status: "complete",
818
- message: "Authentication successful"
819
- });
820
- resolve({
821
- success: true,
822
- user: data.user
823
- });
824
- } else {
825
- const error = new Error(data.error || "Authentication failed");
826
- onStateChange?.({
827
- status: "error",
828
- message: error.message,
829
- error
737
+
738
+ // src/core/static/init.ts
739
+ async function getPlaycademyConfig(allowedParentOrigins) {
740
+ const preloaded = window.PLAYCADEMY;
741
+ if (preloaded?.token) {
742
+ return preloaded;
743
+ }
744
+ if (window.self !== window.top) {
745
+ return await waitForPlaycademyInit(allowedParentOrigins);
746
+ } else {
747
+ return createStandaloneConfig();
748
+ }
749
+ }
750
+ function getReferrerOrigin() {
751
+ try {
752
+ return document.referrer ? new URL(document.referrer).origin : null;
753
+ } catch {
754
+ return null;
755
+ }
756
+ }
757
+ function buildAllowedOrigins(explicit) {
758
+ if (Array.isArray(explicit) && explicit.length > 0)
759
+ return explicit;
760
+ const ref = getReferrerOrigin();
761
+ return ref ? [ref] : [];
762
+ }
763
+ function isOriginAllowed(origin, allowlist) {
764
+ if (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1") {
765
+ return true;
766
+ }
767
+ if (!allowlist || allowlist.length === 0) {
768
+ console.error("[Playcademy SDK] No allowed origins configured. Consider passing allowedParentOrigins explicitly to init().");
769
+ return false;
770
+ }
771
+ return allowlist.includes(origin);
772
+ }
773
+ async function waitForPlaycademyInit(allowedParentOrigins) {
774
+ return new Promise((resolve, reject) => {
775
+ let contextReceived = false;
776
+ const timeoutDuration = 5000;
777
+ const allowlist = buildAllowedOrigins(allowedParentOrigins);
778
+ let hasWarnedAboutUntrustedOrigin = false;
779
+ function warnAboutUntrustedOrigin(origin) {
780
+ if (hasWarnedAboutUntrustedOrigin)
781
+ return;
782
+ hasWarnedAboutUntrustedOrigin = true;
783
+ console.warn("[Playcademy SDK] Ignoring INIT from untrusted origin:", origin);
784
+ }
785
+ const handleMessage = (event) => {
786
+ if (event.data?.type !== "PLAYCADEMY_INIT" /* INIT */)
787
+ return;
788
+ if (!isOriginAllowed(event.origin, allowlist)) {
789
+ warnAboutUntrustedOrigin(event.origin);
790
+ return;
791
+ }
792
+ contextReceived = true;
793
+ window.removeEventListener("message", handleMessage);
794
+ clearTimeout(timeoutId);
795
+ window.PLAYCADEMY = event.data.payload;
796
+ resolve(event.data.payload);
797
+ };
798
+ window.addEventListener("message", handleMessage);
799
+ const timeoutId = setTimeout(() => {
800
+ if (!contextReceived) {
801
+ window.removeEventListener("message", handleMessage);
802
+ reject(new Error(`${"PLAYCADEMY_INIT" /* INIT */} not received within ${timeoutDuration}ms`));
803
+ }
804
+ }, timeoutDuration);
805
+ });
806
+ }
807
+ function createStandaloneConfig() {
808
+ console.debug("[Playcademy SDK] Standalone mode detected, creating mock context for sandbox development");
809
+ const mockConfig = {
810
+ baseUrl: "http://localhost:4321",
811
+ gameUrl: window.location.origin,
812
+ token: "mock-game-token-for-local-dev",
813
+ gameId: "mock-game-id-from-template",
814
+ realtimeUrl: undefined
815
+ };
816
+ window.PLAYCADEMY = mockConfig;
817
+ return mockConfig;
818
+ }
819
+ async function init(options) {
820
+ if (typeof window === "undefined") {
821
+ throw new Error("Playcademy SDK must run in a browser context");
822
+ }
823
+ const config = await getPlaycademyConfig(options?.allowedParentOrigins);
824
+ if (options?.baseUrl) {
825
+ config.baseUrl = options.baseUrl;
826
+ }
827
+ const client = new this({
828
+ baseUrl: config.baseUrl,
829
+ gameUrl: config.gameUrl,
830
+ token: config.token,
831
+ gameId: config.gameId,
832
+ autoStartSession: window.self !== window.top,
833
+ onDisconnect: options?.onDisconnect,
834
+ enableConnectionMonitoring: options?.enableConnectionMonitoring
835
+ });
836
+ client["initPayload"] = config;
837
+ messaging.listen("PLAYCADEMY_TOKEN_REFRESH" /* TOKEN_REFRESH */, ({ token }) => client.setToken(token));
838
+ messaging.send("PLAYCADEMY_READY" /* READY */, undefined);
839
+ return client;
840
+ }
841
+ // src/core/static/login.ts
842
+ async function login(baseUrl, email, password) {
843
+ let url = baseUrl;
844
+ if (baseUrl.startsWith("/") && typeof window !== "undefined") {
845
+ url = window.location.origin + baseUrl;
846
+ }
847
+ url = url + "/auth/login";
848
+ const response = await fetch(url, {
849
+ method: "POST",
850
+ headers: {
851
+ "Content-Type": "application/json"
852
+ },
853
+ body: JSON.stringify({ email, password })
854
+ });
855
+ if (!response.ok) {
856
+ try {
857
+ const errorData = await response.json();
858
+ const errorMessage = errorData && errorData.message ? String(errorData.message) : response.statusText;
859
+ throw new PlaycademyError(errorMessage);
860
+ } catch (error) {
861
+ log.error("[Playcademy SDK] Failed to parse error response JSON, using status text instead:", { error });
862
+ throw new PlaycademyError(response.statusText);
863
+ }
864
+ }
865
+ return response.json();
866
+ }
867
+ // ../utils/src/random.ts
868
+ async function generateSecureRandomString(length) {
869
+ const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
870
+ const randomValues = new Uint8Array(length);
871
+ globalThis.crypto.getRandomValues(randomValues);
872
+ return Array.from(randomValues).map((byte) => charset[byte % charset.length]).join("");
873
+ }
874
+
875
+ // src/core/auth/oauth.ts
876
+ function getTimebackConfig() {
877
+ return {
878
+ authorizationEndpoint: "https://alpha-auth-production-idp.auth.us-west-2.amazoncognito.com/oauth2/authorize",
879
+ tokenEndpoint: "https://alpha-auth-production-idp.auth.us-west-2.amazoncognito.com/oauth2/token",
880
+ scope: "openid email phone"
881
+ };
882
+ }
883
+ var OAUTH_CONFIGS = {
884
+ get TIMEBACK() {
885
+ return getTimebackConfig;
886
+ }
887
+ };
888
+ async function generateOAuthState(data) {
889
+ const csrfToken = await generateSecureRandomString(32);
890
+ if (data && Object.keys(data).length > 0) {
891
+ const jsonStr = JSON.stringify(data);
892
+ const base64 = btoa(jsonStr).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
893
+ return `${csrfToken}.${base64}`;
894
+ }
895
+ return csrfToken;
896
+ }
897
+ function parseOAuthState(state) {
898
+ const lastDotIndex = state.lastIndexOf(".");
899
+ if (lastDotIndex > 0 && lastDotIndex < state.length - 1) {
900
+ try {
901
+ const csrfToken = state.substring(0, lastDotIndex);
902
+ const base64 = state.substring(lastDotIndex + 1);
903
+ const base64WithPadding = base64.replace(/-/g, "+").replace(/_/g, "/");
904
+ const paddedBase64 = base64WithPadding + "=".repeat((4 - base64WithPadding.length % 4) % 4);
905
+ const jsonStr = atob(paddedBase64);
906
+ const data = JSON.parse(jsonStr);
907
+ return { csrfToken, data };
908
+ } catch {}
909
+ }
910
+ return { csrfToken: state };
911
+ }
912
+ function getOAuthConfig(provider) {
913
+ const configGetter = OAUTH_CONFIGS[provider];
914
+ if (!configGetter)
915
+ throw new Error(`Unsupported auth provider: ${provider}`);
916
+ return configGetter();
917
+ }
918
+
919
+ // src/core/static/identity.ts
920
+ var identity = {
921
+ parseOAuthState
922
+ };
923
+ // src/core/auth/flows/popup.ts
924
+ async function initiatePopupFlow(options) {
925
+ const { provider, callbackUrl, onStateChange, oauth } = options;
926
+ try {
927
+ onStateChange?.({
928
+ status: "opening_popup",
929
+ message: "Opening authentication window..."
930
+ });
931
+ const defaults = getOAuthConfig(provider);
932
+ const config = oauth ? { ...defaults, ...oauth } : defaults;
933
+ if (!config.clientId) {
934
+ throw new Error(`clientId is required for ${provider} authentication. ` + "Please provide it in the oauth parameter.");
935
+ }
936
+ const stateData = options.stateData;
937
+ const state = await generateOAuthState(stateData);
938
+ const params = new URLSearchParams({
939
+ response_type: "code",
940
+ client_id: config.clientId,
941
+ redirect_uri: callbackUrl,
942
+ state
943
+ });
944
+ if (config.scope) {
945
+ params.set("scope", config.scope);
946
+ }
947
+ const authUrl = `${config.authorizationEndpoint}?${params.toString()}`;
948
+ const popup = openPopupWindow(authUrl, "playcademy-auth");
949
+ if (!popup || popup.closed) {
950
+ throw new Error("Popup blocked. Please enable popups and try again.");
951
+ }
952
+ onStateChange?.({
953
+ status: "exchanging_token",
954
+ message: "Waiting for authentication..."
955
+ });
956
+ return await waitForServerMessage(popup, onStateChange);
957
+ } catch (error) {
958
+ const errorMessage = error instanceof Error ? error.message : "Authentication failed";
959
+ onStateChange?.({
960
+ status: "error",
961
+ message: errorMessage,
962
+ error: error instanceof Error ? error : new Error(errorMessage)
963
+ });
964
+ throw error;
965
+ }
966
+ }
967
+ async function waitForServerMessage(popup, onStateChange) {
968
+ return new Promise((resolve) => {
969
+ let resolved = false;
970
+ const handleMessage = (event) => {
971
+ if (event.origin !== window.location.origin)
972
+ return;
973
+ const data = event.data;
974
+ if (data?.type === "PLAYCADEMY_AUTH_STATE_CHANGE") {
975
+ resolved = true;
976
+ window.removeEventListener("message", handleMessage);
977
+ if (data.authenticated && data.user) {
978
+ onStateChange?.({
979
+ status: "complete",
980
+ message: "Authentication successful"
981
+ });
982
+ resolve({
983
+ success: true,
984
+ user: data.user
985
+ });
986
+ } else {
987
+ const error = new Error(data.error || "Authentication failed");
988
+ onStateChange?.({
989
+ status: "error",
990
+ message: error.message,
991
+ error
830
992
  });
831
993
  resolve({
832
994
  success: false,
@@ -870,9 +1032,6 @@ async function waitForServerMessage(popup, onStateChange) {
870
1032
  }, 5 * 60 * 1000);
871
1033
  });
872
1034
  }
873
- var init_popup = __esm(() => {
874
- init_oauth();
875
- });
876
1035
 
877
1036
  // src/core/auth/flows/redirect.ts
878
1037
  async function initiateRedirectFlow(options) {
@@ -911,9 +1070,6 @@ async function initiateRedirectFlow(options) {
911
1070
  throw error;
912
1071
  }
913
1072
  }
914
- var init_redirect = __esm(() => {
915
- init_oauth();
916
- });
917
1073
 
918
1074
  // src/core/auth/flows/unified.ts
919
1075
  async function initiateUnifiedFlow(options) {
@@ -928,13 +1084,9 @@ async function initiateUnifiedFlow(options) {
928
1084
  throw new Error(`Unsupported authentication mode: ${effectiveMode}`);
929
1085
  }
930
1086
  }
931
- var init_unified = __esm(() => {
932
- init_popup();
933
- init_redirect();
934
- });
935
1087
 
936
1088
  // src/core/auth/login.ts
937
- async function login(client, options) {
1089
+ async function login2(client, options) {
938
1090
  try {
939
1091
  let stateData = options.stateData;
940
1092
  if (!stateData) {
@@ -973,25 +1125,17 @@ async function login(client, options) {
973
1125
  };
974
1126
  }
975
1127
  }
976
- var init_login = __esm(() => {
977
- init_src();
978
- init_unified();
979
- });
980
1128
 
981
- // src/core/namespaces/identity.ts
1129
+ // src/namespaces/game/identity.ts
982
1130
  function createIdentityNamespace(client) {
983
1131
  return {
984
- connect: (options) => login(client, options),
1132
+ connect: (options) => login2(client, options),
985
1133
  _getContext: () => ({
986
1134
  isInIframe: client["authContext"]?.isInIframe ?? false
987
1135
  })
988
1136
  };
989
1137
  }
990
- var init_identity = __esm(() => {
991
- init_login();
992
- });
993
-
994
- // src/core/namespaces/runtime.ts
1138
+ // src/namespaces/game/runtime.ts
995
1139
  function createRuntimeNamespace(client) {
996
1140
  const eventListeners = new Map;
997
1141
  const trackListener = (eventType, handler) => {
@@ -1040,7 +1184,7 @@ function createRuntimeNamespace(client) {
1040
1184
  exit: async () => {
1041
1185
  if (client["internalClientSessionId"] && client["gameId"]) {
1042
1186
  try {
1043
- await client.games.endSession(client["internalClientSessionId"], client["gameId"]);
1187
+ await client["_sessionManager"].endSession(client["internalClientSessionId"], client["gameId"]);
1044
1188
  } catch (error) {
1045
1189
  log.error("[Playcademy SDK] Failed to auto-end session:", {
1046
1190
  sessionId: client["internalClientSessionId"],
@@ -1145,262 +1289,46 @@ function createRuntimeNamespace(client) {
1145
1289
  }
1146
1290
  };
1147
1291
  }
1148
- var init_runtime = __esm(() => {
1149
- init_src();
1150
- init_messaging();
1151
- });
1152
-
1153
- // src/core/cache/ttl-cache.ts
1154
- function createTTLCache(options) {
1155
- const cache = new Map;
1156
- const { ttl: defaultTTL, keyPrefix = "", onClear } = options;
1157
- async function get(key, loader, config) {
1158
- const fullKey = keyPrefix ? `${keyPrefix}:${key}` : key;
1159
- const now = Date.now();
1160
- const effectiveTTL = config?.ttl !== undefined ? config.ttl : defaultTTL;
1161
- const force = config?.force || false;
1162
- const skipCache = config?.skipCache || false;
1163
- if (effectiveTTL === 0 || skipCache) {
1164
- return loader();
1165
- }
1166
- if (!force) {
1167
- const cached = cache.get(fullKey);
1168
- if (cached && cached.expiresAt > now) {
1169
- return cached.value;
1170
- }
1171
- }
1172
- const promise = loader().catch((error) => {
1173
- cache.delete(fullKey);
1174
- throw error;
1175
- });
1176
- cache.set(fullKey, {
1177
- value: promise,
1178
- expiresAt: now + effectiveTTL
1179
- });
1180
- return promise;
1181
- }
1182
- function clear(key) {
1183
- if (key === undefined) {
1184
- cache.clear();
1185
- onClear?.();
1186
- } else {
1187
- const fullKey = keyPrefix ? `${keyPrefix}:${key}` : key;
1188
- cache.delete(fullKey);
1189
- }
1190
- }
1191
- function size() {
1192
- return cache.size;
1193
- }
1194
- function prune() {
1195
- const now = Date.now();
1196
- for (const [key, entry] of cache.entries()) {
1197
- if (entry.expiresAt <= now) {
1198
- cache.delete(key);
1199
- }
1200
- }
1201
- }
1202
- function getKeys() {
1203
- const keys = [];
1204
- const prefixLen = keyPrefix ? keyPrefix.length + 1 : 0;
1205
- for (const fullKey of cache.keys()) {
1206
- keys.push(fullKey.substring(prefixLen));
1207
- }
1208
- return keys;
1209
- }
1210
- function has(key) {
1211
- const fullKey = keyPrefix ? `${keyPrefix}:${key}` : key;
1212
- const cached = cache.get(fullKey);
1213
- if (!cached)
1214
- return false;
1215
- const now = Date.now();
1216
- if (cached.expiresAt <= now) {
1217
- cache.delete(fullKey);
1218
- return false;
1219
- }
1220
- return true;
1221
- }
1222
- return { get, clear, size, prune, getKeys, has };
1223
- }
1224
-
1225
- // src/core/request.ts
1226
- function checkDevWarnings(data) {
1227
- if (!data || typeof data !== "object")
1228
- return;
1229
- const response = data;
1230
- const warningType = response.__playcademyDevWarning;
1231
- if (!warningType)
1232
- return;
1233
- switch (warningType) {
1234
- case "timeback-not-configured":
1235
- console.warn("%c⚠️ TimeBack Not Configured", "background: #f59e0b; color: white; padding: 6px 12px; border-radius: 4px; font-weight: bold; font-size: 13px");
1236
- console.log("%cTimeBack is configured in playcademy.config.js but the sandbox does not have TimeBack credentials.", "color: #f59e0b; font-weight: 500");
1237
- console.log("To test TimeBack locally:");
1238
- console.log(" Set the following environment variables:");
1239
- console.log(" • %cTIMEBACK_ONEROSTER_API_URL", "color: #0ea5e9; font-weight: 600; font-family: monospace");
1240
- console.log(" • %cTIMEBACK_CALIPER_API_URL", "color: #0ea5e9; font-weight: 600; font-family: monospace");
1241
- console.log(" • %cTIMEBACK_API_CLIENT_ID/SECRET", "color: #0ea5e9; font-weight: 600; font-family: monospace");
1242
- console.log(" Or deploy your game: %cplaycademy deploy", "color: #10b981; font-weight: 600; font-family: monospace");
1243
- console.log(" Or wait for %c@superbuilders/timeback-local%c (coming soon)", "color: #8b5cf6; font-weight: 600; font-family: monospace", "color: inherit");
1244
- break;
1245
- default:
1246
- console.warn(`[Playcademy Dev Warning] ${warningType}`);
1247
- }
1248
- }
1249
- function prepareRequestBody(body, headers) {
1250
- if (body instanceof FormData) {
1251
- return body;
1252
- }
1253
- if (body instanceof ArrayBuffer || body instanceof Blob || ArrayBuffer.isView(body)) {
1254
- if (!headers["Content-Type"]) {
1255
- headers["Content-Type"] = "application/octet-stream";
1256
- }
1257
- return body;
1258
- }
1259
- if (body !== undefined && body !== null) {
1260
- headers["Content-Type"] = "application/json";
1261
- return JSON.stringify(body);
1262
- }
1263
- return;
1264
- }
1265
- async function request({
1266
- path,
1267
- baseUrl,
1268
- method = "GET",
1269
- body,
1270
- extraHeaders = {},
1271
- raw = false
1272
- }) {
1273
- const url = baseUrl.replace(/\/$/, "") + (path.startsWith("/") ? path : `/${path}`);
1274
- const headers = { ...extraHeaders };
1275
- const payload = prepareRequestBody(body, headers);
1276
- const res = await fetch(url, {
1277
- method,
1278
- headers,
1279
- body: payload,
1280
- credentials: "omit"
1281
- });
1282
- if (raw) {
1283
- return res;
1284
- }
1285
- if (!res.ok) {
1286
- const clonedRes = res.clone();
1287
- const errorBody = await clonedRes.json().catch(() => clonedRes.text().catch(() => {
1288
- return;
1289
- })) ?? undefined;
1290
- throw new ApiError(res.status, res.statusText, errorBody);
1291
- }
1292
- if (res.status === 204)
1293
- return;
1294
- const contentType = res.headers.get("content-type") ?? "";
1295
- if (contentType.includes("application/json")) {
1296
- try {
1297
- const parsed = await res.json();
1298
- checkDevWarnings(parsed);
1299
- return parsed;
1300
- } catch (err) {
1301
- if (err instanceof SyntaxError)
1302
- return;
1303
- throw err;
1304
- }
1305
- }
1306
- const rawText = await res.text().catch(() => "");
1307
- return rawText && rawText.length > 0 ? rawText : undefined;
1308
- }
1309
- async function fetchManifest(deploymentUrl) {
1310
- const manifestUrl = `${deploymentUrl.replace(/\/$/, "")}/playcademy.manifest.json`;
1311
- try {
1312
- const response = await fetch(manifestUrl);
1313
- if (!response.ok) {
1314
- log.error(`[fetchManifest] Failed to fetch manifest from ${manifestUrl}. Status: ${response.status}`);
1315
- throw new PlaycademyError(`Failed to fetch manifest: ${response.status} ${response.statusText}`);
1316
- }
1317
- return await response.json();
1318
- } catch (error) {
1319
- if (error instanceof PlaycademyError) {
1320
- throw error;
1321
- }
1322
- log.error(`[Playcademy SDK] Error fetching or parsing manifest from ${manifestUrl}:`, {
1323
- error
1324
- });
1325
- throw new PlaycademyError("Failed to load or parse game manifest");
1292
+ // src/namespaces/game/backend.ts
1293
+ function createBackendNamespace(client) {
1294
+ function normalizePath(path) {
1295
+ return path.startsWith("/") ? path : `/${path}`;
1326
1296
  }
1327
- }
1328
- var init_request = __esm(() => {
1329
- init_src();
1330
- init_errors();
1331
- });
1332
-
1333
- // src/core/namespaces/games.ts
1334
- function createGamesNamespace(client) {
1335
- const gamesListCache = createTTLCache({
1336
- ttl: 60 * 1000,
1337
- keyPrefix: "games.list"
1338
- });
1339
- const gameFetchCache = createTTLCache({
1340
- ttl: 60 * 1000,
1341
- keyPrefix: "games.fetch"
1342
- });
1343
1297
  return {
1344
- fetch: async (gameIdOrSlug, options) => {
1345
- const promise = client["request"](`/games/${gameIdOrSlug}`, "GET");
1346
- return gameFetchCache.get(gameIdOrSlug, async () => {
1347
- const baseGameData = await promise;
1348
- if (baseGameData.gameType === "hosted" && baseGameData.deploymentUrl !== null && baseGameData.deploymentUrl !== "") {
1349
- const manifestData = await fetchManifest(baseGameData.deploymentUrl);
1350
- return { ...baseGameData, manifest: manifestData };
1351
- }
1352
- return baseGameData;
1353
- }, options);
1298
+ async get(path, headers) {
1299
+ return client["requestGameBackend"](normalizePath(path), "GET", undefined, headers);
1354
1300
  },
1355
- list: (options) => {
1356
- return gamesListCache.get("all", () => client["request"]("/games", "GET"), options);
1301
+ async post(path, body, headers) {
1302
+ return client["requestGameBackend"](normalizePath(path), "POST", body, headers);
1357
1303
  },
1358
- saveState: async (state) => {
1359
- const gameId = client["_ensureGameId"]();
1360
- await client["request"](`/games/${gameId}/state`, "POST", { body: state });
1304
+ async put(path, body, headers) {
1305
+ return client["requestGameBackend"](normalizePath(path), "PUT", body, headers);
1361
1306
  },
1362
- loadState: async () => {
1363
- const gameId = client["_ensureGameId"]();
1364
- return client["request"](`/games/${gameId}/state`, "GET");
1307
+ async patch(path, body, headers) {
1308
+ return client["requestGameBackend"](normalizePath(path), "PATCH", body, headers);
1365
1309
  },
1366
- startSession: async (gameId) => {
1367
- const idToUse = gameId ?? client["_ensureGameId"]();
1368
- return client["request"](`/games/${idToUse}/sessions`, "POST", {});
1310
+ async delete(path, headers) {
1311
+ return client["requestGameBackend"](normalizePath(path), "DELETE", undefined, headers);
1369
1312
  },
1370
- endSession: async (sessionId, gameId) => {
1371
- const effectiveGameIdToEnd = gameId ?? client["_ensureGameId"]();
1372
- if (client["internalClientSessionId"] && sessionId === client["internalClientSessionId"] && effectiveGameIdToEnd === client["gameId"]) {
1373
- client["internalClientSessionId"] = undefined;
1374
- }
1375
- await client["request"](`/games/${effectiveGameIdToEnd}/sessions/${sessionId}/end`, "POST");
1313
+ async request(path, method, body, headers) {
1314
+ return client["requestGameBackend"](normalizePath(path), method, body, headers);
1376
1315
  },
1377
- token: {
1378
- create: async (gameId, options) => {
1379
- const res = await client["request"](`/games/${gameId}/token`, "POST");
1380
- if (options?.apply) {
1381
- client.setToken(res.token);
1382
- }
1383
- return res;
1384
- }
1316
+ async download(path, method = "GET", body, headers) {
1317
+ return client["requestGameBackend"](normalizePath(path), method, body, headers, true);
1385
1318
  },
1386
- leaderboard: {
1387
- get: async (gameId, options) => {
1388
- const params = new URLSearchParams;
1389
- if (options?.limit)
1390
- params.append("limit", String(options.limit));
1391
- if (options?.offset)
1392
- params.append("offset", String(options.offset));
1393
- const queryString = params.toString();
1394
- const path = queryString ? `/games/${gameId}/leaderboard?${queryString}` : `/games/${gameId}/leaderboard`;
1395
- return client["request"](path, "GET");
1319
+ url(pathOrStrings, ...values) {
1320
+ if (Array.isArray(pathOrStrings) && "raw" in pathOrStrings) {
1321
+ const strings = pathOrStrings;
1322
+ const path2 = strings.reduce((acc, str, i) => {
1323
+ return acc + str + (values[i] != null ? String(values[i]) : "");
1324
+ }, "");
1325
+ return `${client.gameUrl}/api${path2.startsWith("/") ? path2 : `/${path2}`}`;
1396
1326
  }
1327
+ const path = pathOrStrings;
1328
+ return `${client.gameUrl}/api${path.startsWith("/") ? path : `/${path}`}`;
1397
1329
  }
1398
1330
  };
1399
1331
  }
1400
- var init_games = __esm(() => {
1401
- init_request();
1402
- });
1403
-
1404
1332
  // src/core/cache/permanent-cache.ts
1405
1333
  function createPermanentCache(keyPrefix) {
1406
1334
  const cache = new Map;
@@ -1442,7 +1370,7 @@ function createPermanentCache(keyPrefix) {
1442
1370
  return { get, clear, has, size, keys };
1443
1371
  }
1444
1372
 
1445
- // src/core/namespaces/users.ts
1373
+ // src/namespaces/game/users.ts
1446
1374
  function createUsersNamespace(client) {
1447
1375
  const itemIdCache = createPermanentCache("items");
1448
1376
  const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -1498,594 +1426,105 @@ function createUsersNamespace(client) {
1498
1426
  const qty = item?.quantity ?? 0;
1499
1427
  return qty >= minQuantity;
1500
1428
  }
1501
- },
1502
- scores: {
1503
- get: async (userIdOrOptions, options) => {
1504
- let userId;
1505
- let queryOptions;
1506
- if (typeof userIdOrOptions === "string") {
1507
- userId = userIdOrOptions;
1508
- queryOptions = options || {};
1509
- } else {
1510
- queryOptions = userIdOrOptions || {};
1511
- const user = await client["request"]("/users/me", "GET");
1512
- userId = user.id;
1513
- }
1514
- const params = new URLSearchParams({
1515
- limit: String(queryOptions.limit || 50)
1516
- });
1517
- if (queryOptions.gameId) {
1518
- params.append("gameId", queryOptions.gameId);
1519
- }
1520
- return client["request"](`/users/${userId}/scores?${params}`, "GET");
1521
- }
1522
1429
  }
1523
1430
  };
1524
1431
  }
1525
- var init_users = () => {};
1432
+ // ../constants/src/overworld.ts
1433
+ var ITEM_SLUGS = {
1434
+ PLAYCADEMY_CREDITS: "PLAYCADEMY_CREDITS",
1435
+ PLAYCADEMY_XP: "PLAYCADEMY_XP",
1436
+ FOUNDING_MEMBER_BADGE: "FOUNDING_MEMBER_BADGE",
1437
+ EARLY_ADOPTER_BADGE: "EARLY_ADOPTER_BADGE",
1438
+ FIRST_GAME_BADGE: "FIRST_GAME_BADGE",
1439
+ COMMON_SWORD: "COMMON_SWORD",
1440
+ SMALL_HEALTH_POTION: "SMALL_HEALTH_POTION",
1441
+ SMALL_BACKPACK: "SMALL_BACKPACK",
1442
+ LAVA_LAMP: "LAVA_LAMP",
1443
+ BOOMBOX: "BOOMBOX",
1444
+ CABIN_BED: "CABIN_BED"
1445
+ };
1446
+ var CURRENCIES = {
1447
+ PRIMARY: ITEM_SLUGS.PLAYCADEMY_CREDITS,
1448
+ XP: ITEM_SLUGS.PLAYCADEMY_XP
1449
+ };
1450
+ var BADGES = {
1451
+ FOUNDING_MEMBER: ITEM_SLUGS.FOUNDING_MEMBER_BADGE,
1452
+ EARLY_ADOPTER: ITEM_SLUGS.EARLY_ADOPTER_BADGE,
1453
+ FIRST_GAME: ITEM_SLUGS.FIRST_GAME_BADGE
1454
+ };
1455
+ // ../constants/src/timeback.ts
1456
+ var TIMEBACK_ROUTES = {
1457
+ END_ACTIVITY: "/integrations/timeback/end-activity"
1458
+ };
1459
+ // src/core/cache/singleton-cache.ts
1460
+ function createSingletonCache() {
1461
+ let cachedValue;
1462
+ let hasValue = false;
1463
+ async function get(loader) {
1464
+ if (hasValue) {
1465
+ return cachedValue;
1466
+ }
1467
+ const value = await loader();
1468
+ cachedValue = value;
1469
+ hasValue = true;
1470
+ return value;
1471
+ }
1472
+ function clear() {
1473
+ cachedValue = undefined;
1474
+ hasValue = false;
1475
+ }
1476
+ function has() {
1477
+ return hasValue;
1478
+ }
1479
+ return { get, clear, has };
1480
+ }
1526
1481
 
1527
- // src/core/namespaces/dev.ts
1528
- function createDevNamespace(client) {
1482
+ // src/namespaces/game/credits.ts
1483
+ function createCreditsNamespace(client) {
1484
+ const creditsIdCache = createSingletonCache();
1485
+ const getCreditsItemId = async () => {
1486
+ return creditsIdCache.get(async () => {
1487
+ const queryParams = new URLSearchParams({ slug: CURRENCIES.PRIMARY });
1488
+ const creditsItem = await client["request"](`/items/resolve?${queryParams.toString()}`, "GET");
1489
+ if (!creditsItem || !creditsItem.id) {
1490
+ throw new Error("Playcademy Credits item not found in catalog");
1491
+ }
1492
+ return creditsItem.id;
1493
+ });
1494
+ };
1529
1495
  return {
1530
- status: {
1531
- apply: () => client["request"]("/dev/apply", "POST"),
1532
- get: async () => {
1533
- const response = await client["request"]("/dev/status", "GET");
1534
- return response.status;
1496
+ balance: async () => {
1497
+ const inventory = await client["request"]("/inventory", "GET");
1498
+ const primaryCurrencyInventoryItem = inventory.find((item) => item.item?.slug === CURRENCIES.PRIMARY);
1499
+ return primaryCurrencyInventoryItem?.quantity ?? 0;
1500
+ },
1501
+ add: async (amount) => {
1502
+ if (amount <= 0) {
1503
+ throw new Error("Amount must be positive");
1535
1504
  }
1505
+ const creditsItemId = await getCreditsItemId();
1506
+ const result = await client["request"]("/inventory/add", "POST", {
1507
+ body: {
1508
+ itemId: creditsItemId,
1509
+ qty: amount
1510
+ }
1511
+ });
1512
+ client["emit"]("inventoryChange", {
1513
+ itemId: creditsItemId,
1514
+ delta: amount,
1515
+ newTotal: result.newTotal
1516
+ });
1517
+ return result.newTotal;
1536
1518
  },
1537
- games: {
1538
- deploy: async (slug, options) => {
1539
- const { metadata, file, backend, hooks } = options;
1540
- hooks?.onEvent?.({ type: "init" });
1541
- let game;
1542
- if (metadata) {
1543
- game = await client["request"](`/games/${slug}`, "PUT", {
1544
- body: metadata
1545
- });
1546
- if (metadata.gameType === "external" && !file && !backend) {
1547
- return game;
1548
- }
1549
- }
1550
- let uploadToken;
1551
- if (file) {
1552
- const fileName = file instanceof File ? file.name : "game.zip";
1553
- const initiateResponse = await client["request"]("/games/uploads/initiate/", "POST", {
1554
- body: {
1555
- fileName,
1556
- gameId: game?.id || slug
1557
- }
1558
- });
1559
- uploadToken = initiateResponse.uploadToken;
1560
- if (hooks?.onEvent && typeof XMLHttpRequest !== "undefined") {
1561
- await new Promise((resolve, reject) => {
1562
- const xhr = new XMLHttpRequest;
1563
- xhr.open("PUT", initiateResponse.presignedUrl, true);
1564
- const contentType = file.type || "application/octet-stream";
1565
- try {
1566
- xhr.setRequestHeader("Content-Type", contentType);
1567
- } catch {}
1568
- xhr.upload.onprogress = (event) => {
1569
- if (event.lengthComputable) {
1570
- const percent = event.loaded / event.total;
1571
- hooks.onEvent?.({
1572
- type: "s3Progress",
1573
- loaded: event.loaded,
1574
- total: event.total,
1575
- percent
1576
- });
1577
- }
1578
- };
1579
- xhr.onload = () => {
1580
- if (xhr.status >= 200 && xhr.status < 300)
1581
- resolve();
1582
- else
1583
- reject(new Error(`File upload failed: ${xhr.status} ${xhr.statusText}`));
1584
- };
1585
- xhr.onerror = () => reject(new Error("File upload failed: network error"));
1586
- xhr.send(file);
1587
- });
1588
- } else {
1589
- const uploadResponse = await fetch(initiateResponse.presignedUrl, {
1590
- method: "PUT",
1591
- body: file,
1592
- headers: {
1593
- "Content-Type": file.type || "application/octet-stream"
1594
- }
1595
- });
1596
- if (!uploadResponse.ok) {
1597
- throw new Error(`File upload failed: ${uploadResponse.status} ${uploadResponse.statusText}`);
1598
- }
1599
- }
1600
- }
1601
- if (uploadToken || backend) {
1602
- const deployUrl = `${client.baseUrl}/games/${slug}/deploy`;
1603
- const authToken = client.getToken();
1604
- const tokenType = client.getTokenType();
1605
- const headers = {
1606
- "Content-Type": "application/json"
1607
- };
1608
- if (authToken) {
1609
- if (tokenType === "apiKey") {
1610
- headers["x-api-key"] = authToken;
1611
- } else {
1612
- headers["Authorization"] = `Bearer ${authToken}`;
1613
- }
1614
- }
1615
- const requestBody = {};
1616
- if (uploadToken)
1617
- requestBody.uploadToken = uploadToken;
1618
- if (metadata)
1619
- requestBody.metadata = metadata;
1620
- if (backend) {
1621
- requestBody.code = backend.code;
1622
- requestBody.config = backend.config;
1623
- if (backend.bindings)
1624
- requestBody.bindings = backend.bindings;
1625
- if (backend.schema)
1626
- requestBody.schema = backend.schema;
1627
- if (backend.secrets)
1628
- requestBody.secrets = backend.secrets;
1629
- }
1630
- const finalizeResponse = await fetch(deployUrl, {
1631
- method: "POST",
1632
- headers,
1633
- body: JSON.stringify(requestBody),
1634
- credentials: "omit"
1635
- });
1636
- if (!finalizeResponse.ok) {
1637
- const errText = await finalizeResponse.text().catch(() => "");
1638
- throw new Error(`Deploy request failed: ${finalizeResponse.status} ${finalizeResponse.statusText}${errText ? ` - ${errText}` : ""}`);
1639
- }
1640
- if (!finalizeResponse.body) {
1641
- throw new Error("Deploy response body missing");
1642
- }
1643
- hooks?.onEvent?.({ type: "finalizeStart" });
1644
- let sawAnyServerEvent = false;
1645
- const reader = finalizeResponse.body.pipeThrough(new TextDecoderStream).getReader();
1646
- let buffer = "";
1647
- while (true) {
1648
- const { done, value } = await reader.read();
1649
- if (done) {
1650
- if (!sawAnyServerEvent) {
1651
- hooks?.onClose?.();
1652
- hooks?.onEvent?.({ type: "close" });
1653
- }
1654
- break;
1655
- }
1656
- buffer += value;
1657
- let eolIndex;
1658
- while ((eolIndex = buffer.indexOf(`
1659
-
1660
- `)) >= 0) {
1661
- const message = buffer.slice(0, eolIndex);
1662
- buffer = buffer.slice(eolIndex + 2);
1663
- const eventLine = message.match(/^event: (.*)$/m);
1664
- const dataLine = message.match(/^data: (.*)$/m);
1665
- if (eventLine && dataLine) {
1666
- const eventType = eventLine[1];
1667
- const eventData = JSON.parse(dataLine[1]);
1668
- if (eventType === "uploadProgress") {
1669
- sawAnyServerEvent = true;
1670
- const percent = (eventData.value ?? 0) / 100;
1671
- hooks?.onEvent?.({
1672
- type: "finalizeProgress",
1673
- percent,
1674
- currentFileLabel: eventData.currentFileLabel || ""
1675
- });
1676
- } else if (eventType === "status") {
1677
- sawAnyServerEvent = true;
1678
- if (eventData.message) {
1679
- hooks?.onEvent?.({
1680
- type: "finalizeStatus",
1681
- message: eventData.message
1682
- });
1683
- }
1684
- } else if (eventType === "complete") {
1685
- sawAnyServerEvent = true;
1686
- reader.cancel();
1687
- return eventData;
1688
- } else if (eventType === "error") {
1689
- sawAnyServerEvent = true;
1690
- reader.cancel();
1691
- throw new Error(eventData.message);
1692
- }
1693
- }
1694
- }
1695
- }
1696
- throw new Error("Deployment completed but no final game data received");
1697
- }
1698
- if (game) {
1699
- return game;
1700
- }
1701
- throw new Error("No deployment actions specified (need metadata, file, or backend)");
1702
- },
1703
- seed: async (slug, code, environment) => {
1704
- return client["request"](`/games/${slug}/seed`, "POST", {
1705
- body: { code, environment }
1706
- });
1707
- },
1708
- upsert: async (slug, metadata) => client["request"](`/games/${slug}`, "PUT", { body: metadata }),
1709
- delete: (gameId) => client["request"](`/games/${gameId}`, "DELETE"),
1710
- secrets: {
1711
- set: async (slug, secrets) => {
1712
- const result = await client["request"](`/games/${slug}/secrets`, "POST", { body: secrets });
1713
- return result.keys;
1714
- },
1715
- list: async (slug) => {
1716
- const result = await client["request"](`/games/${slug}/secrets`, "GET");
1717
- return result.keys;
1718
- },
1719
- get: async (slug) => {
1720
- const result = await client["request"](`/games/${slug}/secrets/values`, "GET");
1721
- return result.secrets;
1722
- },
1723
- delete: async (slug, key) => {
1724
- await client["request"](`/games/${slug}/secrets/${key}`, "DELETE");
1725
- }
1726
- },
1727
- database: {
1728
- reset: async (slug, schema) => {
1729
- return client["request"](`/games/${slug}/database/reset`, "POST", {
1730
- body: { schema }
1731
- });
1732
- }
1733
- },
1734
- bucket: {
1735
- list: async (slug, prefix) => {
1736
- const params = prefix ? `?prefix=${encodeURIComponent(prefix)}` : "";
1737
- const result = await client["request"](`/games/${slug}/bucket${params}`, "GET");
1738
- return result.files;
1739
- },
1740
- get: async (slug, key) => {
1741
- const res = await client["request"](`/games/${slug}/bucket/${encodeURIComponent(key)}`, "GET", { raw: true });
1742
- if (!res.ok) {
1743
- let errorMessage = res.statusText;
1744
- try {
1745
- const errorData = await res.json();
1746
- if (errorData.error) {
1747
- errorMessage = errorData.error;
1748
- } else if (errorData.message) {
1749
- errorMessage = errorData.message;
1750
- }
1751
- } catch {}
1752
- throw new Error(errorMessage);
1753
- }
1754
- return res.arrayBuffer();
1755
- },
1756
- put: async (slug, key, content, contentType) => {
1757
- await client["request"](`/games/${slug}/bucket/${encodeURIComponent(key)}`, "PUT", {
1758
- body: content,
1759
- headers: contentType ? { "Content-Type": contentType } : undefined
1760
- });
1761
- },
1762
- delete: async (slug, key) => {
1763
- await client["request"](`/games/${slug}/bucket/${encodeURIComponent(key)}`, "DELETE");
1764
- }
1765
- },
1766
- domains: {
1767
- add: async (slug, hostname) => {
1768
- return client["request"](`/games/${slug}/domains`, "POST", {
1769
- body: { hostname }
1770
- });
1771
- },
1772
- list: async (slug) => {
1773
- const result = await client["request"](`/games/${slug}/domains`, "GET");
1774
- return result.domains;
1775
- },
1776
- status: async (slug, hostname, refresh) => {
1777
- const params = refresh ? "?refresh=true" : "";
1778
- return client["request"](`/games/${slug}/domains/${hostname}${params}`, "GET");
1779
- },
1780
- delete: async (slug, hostname) => {
1781
- await client["request"](`/games/${slug}/domains/${hostname}`, "DELETE");
1782
- }
1783
- }
1784
- },
1785
- items: {
1786
- create: (gameId, slug, itemData) => client["request"](`/games/${gameId}/items`, "POST", {
1787
- body: {
1788
- slug,
1789
- ...itemData
1790
- }
1791
- }),
1792
- update: (gameId, itemId, updates) => client["request"](`/games/${gameId}/items/${itemId}`, "PATCH", {
1793
- body: updates
1794
- }),
1795
- list: (gameId) => client["request"](`/games/${gameId}/items`, "GET"),
1796
- get: (gameId, slug) => {
1797
- const queryParams = new URLSearchParams({ slug, gameId });
1798
- return client["request"](`/items/resolve?${queryParams.toString()}`, "GET");
1799
- },
1800
- delete: (gameId, itemId) => client["request"](`/games/${gameId}/items/${itemId}`, "DELETE"),
1801
- shop: {
1802
- create: (gameId, itemId, listingData) => {
1803
- return client["request"](`/games/${gameId}/items/${itemId}/shop-listing`, "POST", { body: listingData });
1804
- },
1805
- get: (gameId, itemId) => {
1806
- return client["request"](`/games/${gameId}/items/${itemId}/shop-listing`, "GET");
1807
- },
1808
- update: (gameId, itemId, updates) => {
1809
- return client["request"](`/games/${gameId}/items/${itemId}/shop-listing`, "PATCH", { body: updates });
1810
- },
1811
- delete: (gameId, itemId) => {
1812
- return client["request"](`/games/${gameId}/items/${itemId}/shop-listing`, "DELETE");
1813
- },
1814
- list: (gameId) => {
1815
- return client["request"](`/games/${gameId}/shop-listings`, "GET");
1816
- }
1817
- }
1818
- }
1819
- };
1820
- }
1821
-
1822
- // src/core/namespaces/maps.ts
1823
- function createMapsNamespace(client) {
1824
- const mapDataCache = createTTLCache({
1825
- ttl: 5 * 60 * 1000,
1826
- keyPrefix: "maps.data"
1827
- });
1828
- const mapElementsCache = createTTLCache({
1829
- ttl: 60 * 1000,
1830
- keyPrefix: "maps.elements"
1831
- });
1832
- return {
1833
- get: (identifier, options) => mapDataCache.get(identifier, () => client["request"](`/maps/${identifier}`, "GET"), options),
1834
- elements: (mapId, options) => mapElementsCache.get(mapId, () => client["request"](`/map/elements?mapId=${mapId}`, "GET"), options),
1835
- objects: {
1836
- list: (mapId) => client["request"](`/maps/${mapId}/objects`, "GET"),
1837
- create: (mapId, objectData) => client["request"](`/maps/${mapId}/objects`, "POST", {
1838
- body: objectData
1839
- }),
1840
- delete: (mapId, objectId) => client["request"](`/maps/${mapId}/objects/${objectId}`, "DELETE")
1841
- }
1842
- };
1843
- }
1844
- var init_maps = () => {};
1845
-
1846
- // src/core/namespaces/admin.ts
1847
- function createAdminNamespace(client) {
1848
- return {
1849
- games: {
1850
- pauseGame: (gameId) => client["request"](`/admin/games/${gameId}/pause`, "POST"),
1851
- resumeGame: (gameId) => client["request"](`/admin/games/${gameId}/resume`, "POST")
1852
- },
1853
- items: {
1854
- create: (props) => client["request"]("/items", "POST", { body: props }),
1855
- get: (itemId) => client["request"](`/items/${itemId}`, "GET"),
1856
- list: () => client["request"]("/items", "GET"),
1857
- update: (itemId, props) => client["request"](`/items/${itemId}`, "PATCH", { body: props }),
1858
- delete: (itemId) => client["request"](`/items/${itemId}`, "DELETE")
1859
- },
1860
- currencies: {
1861
- create: (props) => client["request"]("/currencies", "POST", { body: props }),
1862
- get: (currencyId) => client["request"](`/currencies/${currencyId}`, "GET"),
1863
- list: () => client["request"]("/currencies", "GET"),
1864
- update: (currencyId, props) => client["request"](`/currencies/${currencyId}`, "PATCH", { body: props }),
1865
- delete: (currencyId) => client["request"](`/currencies/${currencyId}`, "DELETE")
1866
- },
1867
- shopListings: {
1868
- create: (props) => client["request"]("/shop-listings", "POST", { body: props }),
1869
- get: (listingId) => client["request"](`/shop-listings/${listingId}`, "GET"),
1870
- list: () => client["request"]("/shop-listings", "GET"),
1871
- update: (listingId, props) => client["request"](`/shop-listings/${listingId}`, "PATCH", {
1872
- body: props
1873
- }),
1874
- delete: (listingId) => client["request"](`/shop-listings/${listingId}`, "DELETE")
1875
- }
1876
- };
1877
- }
1878
-
1879
- // src/core/namespaces/shop.ts
1880
- function createShopNamespace(client) {
1881
- return {
1882
- view: () => {
1883
- return client["request"]("/shop/view", "GET");
1884
- }
1885
- };
1886
- }
1887
-
1888
- // src/core/namespaces/telemetry.ts
1889
- function createTelemetryNamespace(client) {
1890
- return {
1891
- pushMetrics: (metrics) => client["request"](`/telemetry/metrics`, "POST", { body: metrics })
1892
- };
1893
- }
1894
-
1895
- // src/core/cache/cooldown-cache.ts
1896
- function createCooldownCache(defaultCooldownMs) {
1897
- const lastFetchTime = new Map;
1898
- const pendingRequests = new Map;
1899
- const lastResults = new Map;
1900
- async function get(key, loader, config) {
1901
- const now = Date.now();
1902
- const lastFetch = lastFetchTime.get(key) || 0;
1903
- const timeSinceLastFetch = now - lastFetch;
1904
- const effectiveCooldown = config?.cooldown !== undefined ? config.cooldown : defaultCooldownMs;
1905
- const force = config?.force || false;
1906
- const pending = pendingRequests.get(key);
1907
- if (pending) {
1908
- return pending;
1909
- }
1910
- if (!force && timeSinceLastFetch < effectiveCooldown) {
1911
- const cachedResult = lastResults.get(key);
1912
- if (cachedResult !== undefined) {
1913
- return Promise.resolve(cachedResult);
1914
- }
1915
- }
1916
- const promise = loader().then((result) => {
1917
- pendingRequests.delete(key);
1918
- lastFetchTime.set(key, Date.now());
1919
- lastResults.set(key, result);
1920
- return result;
1921
- }).catch((error) => {
1922
- pendingRequests.delete(key);
1923
- throw error;
1924
- });
1925
- pendingRequests.set(key, promise);
1926
- return promise;
1927
- }
1928
- function clear(key) {
1929
- if (key === undefined) {
1930
- lastFetchTime.clear();
1931
- pendingRequests.clear();
1932
- lastResults.clear();
1933
- } else {
1934
- lastFetchTime.delete(key);
1935
- pendingRequests.delete(key);
1936
- lastResults.delete(key);
1937
- }
1938
- }
1939
- return { get, clear };
1940
- }
1941
-
1942
- // src/core/namespaces/levels.ts
1943
- function createLevelsNamespace(client) {
1944
- const progressCache = createCooldownCache(5000);
1945
- return {
1946
- get: async () => {
1947
- return client["request"]("/users/level", "GET");
1948
- },
1949
- progress: async (options) => {
1950
- return progressCache.get("user-progress", () => client["request"]("/users/level/progress", "GET"), options);
1951
- },
1952
- config: {
1953
- list: async () => {
1954
- return client["request"]("/levels/config", "GET");
1955
- },
1956
- get: async (level) => {
1957
- return client["request"](`/levels/config/${level}`, "GET");
1958
- }
1959
- }
1960
- };
1961
- }
1962
- var init_levels = () => {};
1963
-
1964
- // ../constants/src/auth.ts
1965
- var init_auth = () => {};
1966
-
1967
- // ../constants/src/domains.ts
1968
- var init_domains = () => {};
1969
-
1970
- // ../constants/src/env-vars.ts
1971
- var init_env_vars = () => {};
1972
-
1973
- // ../constants/src/overworld.ts
1974
- var ITEM_SLUGS, CURRENCIES, BADGES;
1975
- var init_overworld = __esm(() => {
1976
- ITEM_SLUGS = {
1977
- PLAYCADEMY_CREDITS: "PLAYCADEMY_CREDITS",
1978
- PLAYCADEMY_XP: "PLAYCADEMY_XP",
1979
- FOUNDING_MEMBER_BADGE: "FOUNDING_MEMBER_BADGE",
1980
- EARLY_ADOPTER_BADGE: "EARLY_ADOPTER_BADGE",
1981
- FIRST_GAME_BADGE: "FIRST_GAME_BADGE",
1982
- COMMON_SWORD: "COMMON_SWORD",
1983
- SMALL_HEALTH_POTION: "SMALL_HEALTH_POTION",
1984
- SMALL_BACKPACK: "SMALL_BACKPACK",
1985
- LAVA_LAMP: "LAVA_LAMP",
1986
- BOOMBOX: "BOOMBOX",
1987
- CABIN_BED: "CABIN_BED"
1988
- };
1989
- CURRENCIES = {
1990
- PRIMARY: ITEM_SLUGS.PLAYCADEMY_CREDITS,
1991
- XP: ITEM_SLUGS.PLAYCADEMY_XP
1992
- };
1993
- BADGES = {
1994
- FOUNDING_MEMBER: ITEM_SLUGS.FOUNDING_MEMBER_BADGE,
1995
- EARLY_ADOPTER: ITEM_SLUGS.EARLY_ADOPTER_BADGE,
1996
- FIRST_GAME: ITEM_SLUGS.FIRST_GAME_BADGE
1997
- };
1998
- });
1999
- // ../constants/src/timeback.ts
2000
- var TIMEBACK_ROUTES;
2001
- var init_timeback = __esm(() => {
2002
- TIMEBACK_ROUTES = {
2003
- END_ACTIVITY: "/integrations/timeback/end-activity"
2004
- };
2005
- });
2006
-
2007
- // ../constants/src/workers.ts
2008
- var init_workers = () => {};
2009
-
2010
- // ../constants/src/index.ts
2011
- var init_src2 = __esm(() => {
2012
- init_auth();
2013
- init_domains();
2014
- init_env_vars();
2015
- init_overworld();
2016
- init_timeback();
2017
- init_workers();
2018
- });
2019
-
2020
- // src/core/cache/singleton-cache.ts
2021
- function createSingletonCache() {
2022
- let cachedValue;
2023
- let hasValue = false;
2024
- async function get(loader) {
2025
- if (hasValue) {
2026
- return cachedValue;
2027
- }
2028
- const value = await loader();
2029
- cachedValue = value;
2030
- hasValue = true;
2031
- return value;
2032
- }
2033
- function clear() {
2034
- cachedValue = undefined;
2035
- hasValue = false;
2036
- }
2037
- function has() {
2038
- return hasValue;
2039
- }
2040
- return { get, clear, has };
2041
- }
2042
-
2043
- // src/core/namespaces/credits.ts
2044
- function createCreditsNamespace(client) {
2045
- const creditsIdCache = createSingletonCache();
2046
- const getCreditsItemId = async () => {
2047
- return creditsIdCache.get(async () => {
2048
- const queryParams = new URLSearchParams({ slug: CURRENCIES.PRIMARY });
2049
- const creditsItem = await client["request"](`/items/resolve?${queryParams.toString()}`, "GET");
2050
- if (!creditsItem || !creditsItem.id) {
2051
- throw new Error("Playcademy Credits item not found in catalog");
2052
- }
2053
- return creditsItem.id;
2054
- });
2055
- };
2056
- return {
2057
- balance: async () => {
2058
- const inventory = await client["request"]("/inventory", "GET");
2059
- const primaryCurrencyInventoryItem = inventory.find((item) => item.item?.slug === CURRENCIES.PRIMARY);
2060
- return primaryCurrencyInventoryItem?.quantity ?? 0;
2061
- },
2062
- add: async (amount) => {
2063
- if (amount <= 0) {
2064
- throw new Error("Amount must be positive");
2065
- }
2066
- const creditsItemId = await getCreditsItemId();
2067
- const result = await client["request"]("/inventory/add", "POST", {
2068
- body: {
2069
- itemId: creditsItemId,
2070
- qty: amount
2071
- }
2072
- });
2073
- client["emit"]("inventoryChange", {
2074
- itemId: creditsItemId,
2075
- delta: amount,
2076
- newTotal: result.newTotal
2077
- });
2078
- return result.newTotal;
2079
- },
2080
- spend: async (amount) => {
2081
- if (amount <= 0) {
2082
- throw new Error("Amount must be positive");
2083
- }
2084
- const creditsItemId = await getCreditsItemId();
2085
- const result = await client["request"]("/inventory/remove", "POST", {
2086
- body: {
2087
- itemId: creditsItemId,
2088
- qty: amount
1519
+ spend: async (amount) => {
1520
+ if (amount <= 0) {
1521
+ throw new Error("Amount must be positive");
1522
+ }
1523
+ const creditsItemId = await getCreditsItemId();
1524
+ const result = await client["request"]("/inventory/remove", "POST", {
1525
+ body: {
1526
+ itemId: creditsItemId,
1527
+ qty: amount
2089
1528
  }
2090
1529
  });
2091
1530
  client["emit"]("inventoryChange", {
@@ -2097,31 +1536,7 @@ function createCreditsNamespace(client) {
2097
1536
  }
2098
1537
  };
2099
1538
  }
2100
- var init_credits = __esm(() => {
2101
- init_src2();
2102
- });
2103
-
2104
- // src/core/namespaces/leaderboard.ts
2105
- function createLeaderboardNamespace(client) {
2106
- return {
2107
- fetch: async (options) => {
2108
- const params = new URLSearchParams({
2109
- timeframe: options?.timeframe || "all_time",
2110
- limit: String(options?.limit || 10),
2111
- offset: String(options?.offset || 0)
2112
- });
2113
- if (options?.gameId) {
2114
- params.append("gameId", options.gameId);
2115
- }
2116
- return client["request"](`/leaderboard?${params}`, "GET");
2117
- },
2118
- getUserRank: async (gameId, userId) => {
2119
- return client["request"](`/games/${gameId}/users/${userId}/rank`, "GET");
2120
- }
2121
- };
2122
- }
2123
-
2124
- // src/core/namespaces/scores.ts
1539
+ // src/namespaces/game/scores.ts
2125
1540
  function createScoresNamespace(client) {
2126
1541
  return {
2127
1542
  submit: async (gameId, score, metadata) => {
@@ -2131,99 +1546,15 @@ function createScoresNamespace(client) {
2131
1546
  metadata
2132
1547
  }
2133
1548
  });
2134
- },
2135
- getByUser: async (gameId, userId, options) => {
2136
- const params = new URLSearchParams;
2137
- if (options?.limit) {
2138
- params.append("limit", String(options.limit));
2139
- }
2140
- const queryString = params.toString();
2141
- const path = queryString ? `/games/${gameId}/users/${userId}/scores?${queryString}` : `/games/${gameId}/users/${userId}/scores`;
2142
- return client["request"](path, "GET");
2143
- }
2144
- };
2145
- }
2146
-
2147
- // src/core/namespaces/character.ts
2148
- function createCharacterNamespace(client) {
2149
- const componentCache = createTTLCache({
2150
- ttl: 5 * 60 * 1000,
2151
- keyPrefix: "character.components"
2152
- });
2153
- return {
2154
- get: async (userId) => {
2155
- try {
2156
- const path = userId ? `/character/${userId}` : "/character";
2157
- return await client["request"](path, "GET");
2158
- } catch (error) {
2159
- if (error instanceof Error) {
2160
- if (error.message.includes("404")) {
2161
- return null;
2162
- }
2163
- }
2164
- throw error;
2165
- }
2166
- },
2167
- create: async (characterData) => {
2168
- return client["request"]("/character", "POST", { body: characterData });
2169
- },
2170
- update: async (updates) => {
2171
- return client["request"]("/character", "PATCH", { body: updates });
2172
- },
2173
- components: {
2174
- list: async (options) => {
2175
- const cacheKey = options?.level === undefined ? "all" : String(options.level);
2176
- return componentCache.get(cacheKey, async () => {
2177
- const path = options?.level !== undefined ? `/character/components?level=${options.level}` : "/character/components";
2178
- const components = await client["request"](path, "GET");
2179
- return components || [];
2180
- }, options);
2181
- },
2182
- clearCache: (key) => componentCache.clear(key),
2183
- getCacheKeys: () => componentCache.getKeys()
2184
- },
2185
- accessories: {
2186
- equip: async (slot, componentId) => {
2187
- return client["request"]("/character/accessories/equip", "POST", { body: { slot, accessoryComponentId: componentId } });
2188
- },
2189
- remove: async (slot) => {
2190
- return client["request"](`/character/accessories/${slot}`, "DELETE");
2191
- },
2192
- list: async () => {
2193
- const character = await client.character.get();
2194
- return character?.accessories || [];
2195
- }
2196
- }
2197
- };
2198
- }
2199
- var init_character = () => {};
2200
-
2201
- // src/core/namespaces/sprites.ts
2202
- function createSpritesNamespace(client) {
2203
- const templateUrlCache = createPermanentCache("sprite-template-urls");
2204
- return {
2205
- templates: {
2206
- get: async (slug) => {
2207
- if (!slug)
2208
- throw new Error("Sprite template slug is required");
2209
- const templateMeta = await templateUrlCache.get(slug, async () => {
2210
- return client["request"](`/sprites/templates/${slug}`, "GET");
2211
- });
2212
- if (!templateMeta.url) {
2213
- throw new Error(`Template ${slug} has no URL in database`);
2214
- }
2215
- const response = await fetch(templateMeta.url);
2216
- if (!response.ok) {
2217
- throw new Error(`Failed to fetch template JSON from ${templateMeta.url}: ${response.statusText}`);
2218
- }
2219
- return await response.json();
2220
- }
2221
1549
  }
2222
1550
  };
2223
1551
  }
2224
- var init_sprites = () => {};
1552
+ // src/namespaces/game/realtime.client.ts
1553
+ var CLOSE_CODES = {
1554
+ NORMAL_CLOSURE: 1000,
1555
+ TOKEN_REFRESH: 4000
1556
+ };
2225
1557
 
2226
- // src/core/namespaces/realtime.client.ts
2227
1558
  class RealtimeChannelClient {
2228
1559
  gameId;
2229
1560
  _channelName;
@@ -2420,17 +1751,8 @@ class RealtimeChannelClient {
2420
1751
  return this.ws?.readyState === WebSocket.OPEN;
2421
1752
  }
2422
1753
  }
2423
- var CLOSE_CODES;
2424
- var init_realtime_client = __esm(() => {
2425
- init_src();
2426
- init_messaging();
2427
- CLOSE_CODES = {
2428
- NORMAL_CLOSURE: 1000,
2429
- TOKEN_REFRESH: 4000
2430
- };
2431
- });
2432
1754
 
2433
- // src/core/namespaces/realtime.ts
1755
+ // src/namespaces/game/realtime.ts
2434
1756
  function createRealtimeNamespace(client) {
2435
1757
  return {
2436
1758
  token: {
@@ -2455,45 +1777,7 @@ function createRealtimeNamespace(client) {
2455
1777
  }
2456
1778
  };
2457
1779
  }
2458
- var init_realtime = __esm(() => {
2459
- init_realtime_client();
2460
- });
2461
-
2462
- // src/core/namespaces/achievements.ts
2463
- function createAchievementsNamespace(client) {
2464
- const achievementsListCache = createTTLCache({
2465
- ttl: 5 * 1000,
2466
- keyPrefix: "achievements.list"
2467
- });
2468
- const achievementsHistoryCache = createTTLCache({
2469
- ttl: 5 * 1000,
2470
- keyPrefix: "achievements.history"
2471
- });
2472
- return {
2473
- list: (options) => {
2474
- return achievementsListCache.get("current", () => client["request"]("/achievements/current", "GET"), options);
2475
- },
2476
- history: {
2477
- list: async (queryOptions, cacheOptions) => {
2478
- const params = new URLSearchParams;
2479
- if (queryOptions?.limit)
2480
- params.append("limit", String(queryOptions.limit));
2481
- const qs = params.toString();
2482
- const path = qs ? `/achievements/history?${qs}` : "/achievements/history";
2483
- const cacheKey = qs ? `history-${qs}` : "history";
2484
- return achievementsHistoryCache.get(cacheKey, () => client["request"](path, "GET"), cacheOptions);
2485
- }
2486
- },
2487
- progress: {
2488
- submit: async (achievementId) => client["request"]("/achievements/progress", "POST", {
2489
- body: { achievementId }
2490
- })
2491
- }
2492
- };
2493
- }
2494
- var init_achievements = () => {};
2495
-
2496
- // src/core/namespaces/timeback.ts
1780
+ // src/namespaces/game/timeback.ts
2497
1781
  function createTimebackNamespace(client) {
2498
1782
  let currentActivity = null;
2499
1783
  return {
@@ -2548,7 +1832,8 @@ function createTimebackNamespace(client) {
2548
1832
  timingData: {
2549
1833
  durationSeconds
2550
1834
  },
2551
- xpEarned: data.xpAwarded
1835
+ xpEarned: data.xpAwarded,
1836
+ masteredUnits: data.masteredUnits
2552
1837
  };
2553
1838
  try {
2554
1839
  const response = await client["requestGameBackend"](TIMEBACK_ROUTES.END_ACTIVITY, "POST", request2);
@@ -2620,515 +1905,197 @@ function createTimebackNamespace(client) {
2620
1905
  }
2621
1906
  };
2622
1907
  }
2623
- var init_timeback2 = __esm(() => {
2624
- init_src2();
2625
- });
2626
-
2627
- // src/core/namespaces/notifications.ts
2628
- function createNotificationsNamespace(client) {
2629
- const notificationsListCache = createTTLCache({
2630
- ttl: 5 * 1000,
2631
- keyPrefix: "notifications.list"
2632
- });
2633
- const notificationStatsCache = createTTLCache({
2634
- ttl: 30 * 1000,
2635
- keyPrefix: "notifications.stats"
2636
- });
2637
- return {
2638
- list: async (queryOptions, cacheOptions) => {
2639
- const params = new URLSearchParams;
2640
- if (queryOptions?.status)
2641
- params.append("status", queryOptions.status);
2642
- if (queryOptions?.type)
2643
- params.append("type", queryOptions.type);
2644
- if (queryOptions?.limit)
2645
- params.append("limit", String(queryOptions.limit));
2646
- if (queryOptions?.offset)
2647
- params.append("offset", String(queryOptions.offset));
2648
- const qs = params.toString();
2649
- const path = qs ? `/notifications?${qs}` : "/notifications";
2650
- const cacheKey = qs ? `list-${qs}` : "list";
2651
- return notificationsListCache.get(cacheKey, () => client["request"](path, "GET"), cacheOptions);
2652
- },
2653
- markAsSeen: async (notificationId) => {
2654
- const result = await client["request"](`/notifications/${notificationId}/status`, "PATCH", {
2655
- body: {
2656
- id: notificationId,
2657
- status: "seen"
2658
- }
2659
- });
2660
- notificationsListCache.clear();
2661
- return result;
2662
- },
2663
- markAsClicked: async (notificationId) => {
2664
- const result = await client["request"](`/notifications/${notificationId}/status`, "PATCH", {
2665
- body: {
2666
- id: notificationId,
2667
- status: "clicked"
2668
- }
2669
- });
2670
- notificationsListCache.clear();
2671
- return result;
2672
- },
2673
- dismiss: async (notificationId) => {
2674
- const result = await client["request"](`/notifications/${notificationId}/status`, "PATCH", {
2675
- body: {
2676
- id: notificationId,
2677
- status: "dismissed"
2678
- }
2679
- });
2680
- notificationsListCache.clear();
2681
- return result;
1908
+ // src/clients/public.ts
1909
+ class PlaycademyClient {
1910
+ baseUrl;
1911
+ gameUrl;
1912
+ authStrategy;
1913
+ gameId;
1914
+ config;
1915
+ listeners = {};
1916
+ internalClientSessionId;
1917
+ authContext;
1918
+ initPayload;
1919
+ connectionManager;
1920
+ _sessionManager = {
1921
+ startSession: async (gameId) => {
1922
+ return this.request(`/games/${gameId}/sessions`, "POST");
2682
1923
  },
2683
- stats: {
2684
- get: async (queryOptions, cacheOptions) => {
2685
- const user = await client.users.me();
2686
- const params = new URLSearchParams;
2687
- if (queryOptions?.from)
2688
- params.append("from", queryOptions.from);
2689
- if (queryOptions?.to)
2690
- params.append("to", queryOptions.to);
2691
- const qs = params.toString();
2692
- const path = qs ? `/notifications/stats/${user.id}?${qs}` : `/notifications/stats/${user.id}`;
2693
- const cacheKey = qs ? `stats-${qs}` : "stats";
2694
- return notificationStatsCache.get(cacheKey, () => client["request"](path, "GET"), cacheOptions);
2695
- }
1924
+ endSession: async (sessionId, gameId) => {
1925
+ return this.request(`/games/${gameId}/sessions/${sessionId}`, "DELETE");
2696
1926
  }
2697
1927
  };
2698
- }
2699
- var init_notifications = () => {};
2700
-
2701
- // src/core/namespaces/backend.ts
2702
- function createBackendNamespace(client) {
2703
- function normalizePath(path) {
2704
- return path.startsWith("/") ? path : `/${path}`;
1928
+ constructor(config) {
1929
+ this.baseUrl = config?.baseUrl?.endsWith("/api") ? config.baseUrl : `${config?.baseUrl}/api`;
1930
+ this.gameUrl = config?.gameUrl;
1931
+ this.gameId = config?.gameId;
1932
+ this.config = config || {};
1933
+ this.authStrategy = createAuthStrategy(config?.token ?? null, config?.tokenType);
1934
+ this._detectAuthContext();
1935
+ this._initializeInternalSession().catch(() => {});
1936
+ this._initializeConnectionMonitor();
1937
+ }
1938
+ getBaseUrl() {
1939
+ const isRelative = this.baseUrl.startsWith("/");
1940
+ const isBrowser2 = typeof window !== "undefined";
1941
+ return isRelative && isBrowser2 ? `${window.location.origin}${this.baseUrl}` : this.baseUrl;
1942
+ }
1943
+ getGameBackendUrl() {
1944
+ if (!this.gameUrl) {
1945
+ throw new PlaycademyError("Game backend URL not configured. gameUrl must be set to use game backend features.");
1946
+ }
1947
+ const isRelative = this.gameUrl.startsWith("/");
1948
+ const isBrowser2 = typeof window !== "undefined";
1949
+ const effectiveGameUrl = isRelative && isBrowser2 ? `${window.location.origin}${this.gameUrl}` : this.gameUrl;
1950
+ return `${effectiveGameUrl}/api`;
1951
+ }
1952
+ ping() {
1953
+ return "pong";
1954
+ }
1955
+ setToken(token, tokenType) {
1956
+ this.authStrategy = createAuthStrategy(token, tokenType);
1957
+ this.emit("authChange", { token });
1958
+ }
1959
+ getTokenType() {
1960
+ return this.authStrategy.getType();
2705
1961
  }
2706
- return {
2707
- async get(path, headers) {
2708
- return client["requestGameBackend"](normalizePath(path), "GET", undefined, headers);
2709
- },
2710
- async post(path, body, headers) {
2711
- return client["requestGameBackend"](normalizePath(path), "POST", body, headers);
2712
- },
2713
- async put(path, body, headers) {
2714
- return client["requestGameBackend"](normalizePath(path), "PUT", body, headers);
2715
- },
2716
- async patch(path, body, headers) {
2717
- return client["requestGameBackend"](normalizePath(path), "PATCH", body, headers);
2718
- },
2719
- async delete(path, headers) {
2720
- return client["requestGameBackend"](normalizePath(path), "DELETE", undefined, headers);
2721
- },
2722
- async request(path, method, body, headers) {
2723
- return client["requestGameBackend"](normalizePath(path), method, body, headers);
2724
- },
2725
- async download(path, method = "GET", body, headers) {
2726
- return client["requestGameBackend"](normalizePath(path), method, body, headers, true);
2727
- },
2728
- url(pathOrStrings, ...values) {
2729
- if (Array.isArray(pathOrStrings) && "raw" in pathOrStrings) {
2730
- const strings = pathOrStrings;
2731
- const path2 = strings.reduce((acc, str, i) => {
2732
- return acc + str + (values[i] != null ? String(values[i]) : "");
2733
- }, "");
2734
- return `${client.gameUrl}/api${path2.startsWith("/") ? path2 : `/${path2}`}`;
2735
- }
2736
- const path = pathOrStrings;
2737
- return `${client.gameUrl}/api${path.startsWith("/") ? path : `/${path}`}`;
2738
- }
2739
- };
2740
- }
2741
-
2742
- // src/core/namespaces/index.ts
2743
- var init_namespaces = __esm(() => {
2744
- init_identity();
2745
- init_runtime();
2746
- init_games();
2747
- init_users();
2748
- init_maps();
2749
- init_levels();
2750
- init_credits();
2751
- init_character();
2752
- init_sprites();
2753
- init_realtime();
2754
- init_achievements();
2755
- init_timeback2();
2756
- init_notifications();
2757
- });
2758
-
2759
- // src/core/static/init.ts
2760
- async function getPlaycademyConfig(allowedParentOrigins) {
2761
- const preloaded = window.PLAYCADEMY;
2762
- if (preloaded?.token) {
2763
- return preloaded;
1962
+ getToken() {
1963
+ return this.authStrategy.getToken();
2764
1964
  }
2765
- if (window.self !== window.top) {
2766
- return await waitForPlaycademyInit(allowedParentOrigins);
2767
- } else {
2768
- return createStandaloneConfig();
1965
+ isAuthenticated() {
1966
+ return this.authStrategy.getToken() !== null;
2769
1967
  }
2770
- }
2771
- function getReferrerOrigin() {
2772
- try {
2773
- return document.referrer ? new URL(document.referrer).origin : null;
2774
- } catch {
2775
- return null;
1968
+ onAuthChange(callback) {
1969
+ this.on("authChange", (payload) => callback(payload.token));
2776
1970
  }
2777
- }
2778
- function buildAllowedOrigins(explicit) {
2779
- if (Array.isArray(explicit) && explicit.length > 0)
2780
- return explicit;
2781
- const ref = getReferrerOrigin();
2782
- return ref ? [ref] : [];
2783
- }
2784
- function isOriginAllowed(origin, allowlist) {
2785
- if (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1") {
2786
- return true;
1971
+ onDisconnect(callback) {
1972
+ if (!this.connectionManager) {
1973
+ return () => {};
1974
+ }
1975
+ return this.connectionManager.onDisconnect(callback);
2787
1976
  }
2788
- if (!allowlist || allowlist.length === 0) {
2789
- console.error("[Playcademy SDK] No allowed origins configured. Consider passing allowedParentOrigins explicitly to init().");
2790
- return false;
1977
+ getConnectionState() {
1978
+ return this.connectionManager?.getState() ?? "unknown";
2791
1979
  }
2792
- return allowlist.includes(origin);
2793
- }
2794
- async function waitForPlaycademyInit(allowedParentOrigins) {
2795
- return new Promise((resolve, reject) => {
2796
- let contextReceived = false;
2797
- const timeoutDuration = 5000;
2798
- const allowlist = buildAllowedOrigins(allowedParentOrigins);
2799
- let hasWarnedAboutUntrustedOrigin = false;
2800
- function warnAboutUntrustedOrigin(origin) {
2801
- if (hasWarnedAboutUntrustedOrigin)
2802
- return;
2803
- hasWarnedAboutUntrustedOrigin = true;
2804
- console.warn("[Playcademy SDK] Ignoring INIT from untrusted origin:", origin);
2805
- }
2806
- const handleMessage = (event) => {
2807
- if (event.data?.type !== "PLAYCADEMY_INIT" /* INIT */)
2808
- return;
2809
- if (!isOriginAllowed(event.origin, allowlist)) {
2810
- warnAboutUntrustedOrigin(event.origin);
2811
- return;
2812
- }
2813
- contextReceived = true;
2814
- window.removeEventListener("message", handleMessage);
2815
- clearTimeout(timeoutId);
2816
- window.PLAYCADEMY = event.data.payload;
2817
- resolve(event.data.payload);
2818
- };
2819
- window.addEventListener("message", handleMessage);
2820
- const timeoutId = setTimeout(() => {
2821
- if (!contextReceived) {
2822
- window.removeEventListener("message", handleMessage);
2823
- reject(new Error(`${"PLAYCADEMY_INIT" /* INIT */} not received within ${timeoutDuration}ms`));
2824
- }
2825
- }, timeoutDuration);
2826
- });
2827
- }
2828
- function createStandaloneConfig() {
2829
- console.debug("[Playcademy SDK] Standalone mode detected, creating mock context for sandbox development");
2830
- const mockConfig = {
2831
- baseUrl: "http://localhost:4321",
2832
- gameUrl: window.location.origin,
2833
- token: "mock-game-token-for-local-dev",
2834
- gameId: "mock-game-id-from-template",
2835
- realtimeUrl: undefined
2836
- };
2837
- window.PLAYCADEMY = mockConfig;
2838
- return mockConfig;
2839
- }
2840
- async function init(options) {
2841
- const { PlaycademyClient } = await Promise.resolve().then(() => (init_client(), exports_client));
2842
- if (typeof window === "undefined") {
2843
- throw new Error("Playcademy SDK must run in a browser context");
1980
+ async checkConnection() {
1981
+ if (!this.connectionManager)
1982
+ return "unknown";
1983
+ return await this.connectionManager.checkNow();
2844
1984
  }
2845
- const config = await getPlaycademyConfig(options?.allowedParentOrigins);
2846
- if (options?.baseUrl) {
2847
- config.baseUrl = options.baseUrl;
1985
+ _setAuthContext(context) {
1986
+ this.authContext = context;
2848
1987
  }
2849
- const client = new PlaycademyClient({
2850
- baseUrl: config.baseUrl,
2851
- gameUrl: config.gameUrl,
2852
- token: config.token,
2853
- gameId: config.gameId,
2854
- autoStartSession: window.self !== window.top,
2855
- onDisconnect: options?.onDisconnect,
2856
- enableConnectionMonitoring: options?.enableConnectionMonitoring
2857
- });
2858
- client["initPayload"] = config;
2859
- messaging.listen("PLAYCADEMY_TOKEN_REFRESH" /* TOKEN_REFRESH */, ({ token }) => client.setToken(token));
2860
- messaging.send("PLAYCADEMY_READY" /* READY */, undefined);
2861
- if (import.meta.env?.MODE === "development") {
2862
- window.PLAYCADEMY_CLIENT = client;
1988
+ on(event, callback) {
1989
+ this.listeners[event] = this.listeners[event] ?? [];
1990
+ this.listeners[event].push(callback);
2863
1991
  }
2864
- return client;
2865
- }
2866
- var init_init = __esm(() => {
2867
- init_messaging();
2868
- });
2869
-
2870
- // src/core/static/login.ts
2871
- async function login2(baseUrl, email, password) {
2872
- let url = baseUrl;
2873
- if (baseUrl.startsWith("/") && typeof window !== "undefined") {
2874
- url = window.location.origin + baseUrl;
1992
+ emit(event, payload) {
1993
+ (this.listeners[event] ?? []).forEach((listener) => {
1994
+ listener(payload);
1995
+ });
2875
1996
  }
2876
- url = url + "/auth/login";
2877
- const response = await fetch(url, {
2878
- method: "POST",
2879
- headers: {
2880
- "Content-Type": "application/json"
2881
- },
2882
- body: JSON.stringify({ email, password })
2883
- });
2884
- if (!response.ok) {
1997
+ async request(path, method, options) {
1998
+ const effectiveHeaders = {
1999
+ ...options?.headers,
2000
+ ...this.authStrategy.getHeaders()
2001
+ };
2885
2002
  try {
2886
- const errorData = await response.json();
2887
- const errorMessage = errorData && errorData.message ? String(errorData.message) : response.statusText;
2888
- throw new PlaycademyError(errorMessage);
2003
+ const result = await request({
2004
+ path,
2005
+ method,
2006
+ body: options?.body,
2007
+ baseUrl: this.baseUrl,
2008
+ extraHeaders: effectiveHeaders,
2009
+ raw: options?.raw
2010
+ });
2011
+ this.connectionManager?.reportRequestSuccess();
2012
+ return result;
2889
2013
  } catch (error) {
2890
- log.error("[Playcademy SDK] Failed to parse error response JSON, using status text instead:", { error });
2891
- throw new PlaycademyError(response.statusText);
2014
+ this.connectionManager?.reportRequestFailure(error);
2015
+ throw error;
2892
2016
  }
2893
2017
  }
2894
- return response.json();
2895
- }
2896
- var init_login2 = __esm(() => {
2897
- init_src();
2898
- init_errors();
2899
- });
2900
-
2901
- // src/core/static/identity.ts
2902
- var identity;
2903
- var init_identity2 = __esm(() => {
2904
- init_oauth();
2905
- identity = {
2906
- parseOAuthState
2907
- };
2908
- });
2909
-
2910
- // src/core/static/index.ts
2911
- var init_static = __esm(() => {
2912
- init_init();
2913
- init_login2();
2914
- init_identity2();
2915
- });
2916
-
2917
- // src/core/client.ts
2918
- var exports_client = {};
2919
- __export(exports_client, {
2920
- PlaycademyClient: () => PlaycademyClient
2921
- });
2922
- var PlaycademyClient;
2923
- var init_client = __esm(() => {
2924
- init_src();
2925
- init_connection();
2926
- init_errors();
2927
- init_namespaces();
2928
- init_request();
2929
- init_static();
2930
- PlaycademyClient = class PlaycademyClient {
2931
- baseUrl;
2932
- gameUrl;
2933
- authStrategy;
2934
- gameId;
2935
- config;
2936
- listeners = {};
2937
- internalClientSessionId;
2938
- authContext;
2939
- initPayload;
2940
- connectionManager;
2941
- constructor(config) {
2942
- this.baseUrl = config?.baseUrl?.endsWith("/api") ? config.baseUrl : `${config?.baseUrl}/api`;
2943
- this.gameUrl = config?.gameUrl;
2944
- this.gameId = config?.gameId;
2945
- this.config = config || {};
2946
- this.authStrategy = createAuthStrategy(config?.token ?? null, config?.tokenType);
2947
- this._detectAuthContext();
2948
- this._initializeInternalSession().catch(() => {});
2949
- this._initializeConnectionMonitor();
2950
- }
2951
- getBaseUrl() {
2952
- const isRelative = this.baseUrl.startsWith("/");
2953
- const isBrowser2 = typeof window !== "undefined";
2954
- return isRelative && isBrowser2 ? `${window.location.origin}${this.baseUrl}` : this.baseUrl;
2955
- }
2956
- getGameBackendUrl() {
2957
- if (!this.gameUrl) {
2958
- throw new PlaycademyError("Game backend URL not configured. gameUrl must be set to use game backend features.");
2959
- }
2960
- const isRelative = this.gameUrl.startsWith("/");
2961
- const isBrowser2 = typeof window !== "undefined";
2962
- const effectiveGameUrl = isRelative && isBrowser2 ? `${window.location.origin}${this.gameUrl}` : this.gameUrl;
2963
- return `${effectiveGameUrl}/api`;
2964
- }
2965
- ping() {
2966
- return "pong";
2967
- }
2968
- setToken(token, tokenType) {
2969
- this.authStrategy = createAuthStrategy(token, tokenType);
2970
- this.emit("authChange", { token });
2971
- }
2972
- getTokenType() {
2973
- return this.authStrategy.getType();
2974
- }
2975
- getToken() {
2976
- return this.authStrategy.getToken();
2977
- }
2978
- isAuthenticated() {
2979
- return this.authStrategy.getToken() !== null;
2980
- }
2981
- onAuthChange(callback) {
2982
- this.on("authChange", (payload) => callback(payload.token));
2983
- }
2984
- onDisconnect(callback) {
2985
- if (!this.connectionManager) {
2986
- return () => {};
2987
- }
2988
- return this.connectionManager.onDisconnect(callback);
2989
- }
2990
- getConnectionState() {
2991
- return this.connectionManager?.getState() ?? "unknown";
2992
- }
2993
- async checkConnection() {
2994
- if (!this.connectionManager)
2995
- return "unknown";
2996
- return await this.connectionManager.checkNow();
2997
- }
2998
- _setAuthContext(context) {
2999
- this.authContext = context;
3000
- }
3001
- on(event, callback) {
3002
- this.listeners[event] = this.listeners[event] ?? [];
3003
- this.listeners[event].push(callback);
3004
- }
3005
- emit(event, payload) {
3006
- (this.listeners[event] ?? []).forEach((listener) => {
3007
- listener(payload);
2018
+ async requestGameBackend(path, method, body, headers, raw) {
2019
+ const effectiveHeaders = {
2020
+ ...headers,
2021
+ ...this.authStrategy.getHeaders()
2022
+ };
2023
+ try {
2024
+ const result = await request({
2025
+ path,
2026
+ method,
2027
+ body,
2028
+ baseUrl: this.getGameBackendUrl(),
2029
+ extraHeaders: effectiveHeaders,
2030
+ raw
3008
2031
  });
2032
+ this.connectionManager?.reportRequestSuccess();
2033
+ return result;
2034
+ } catch (error) {
2035
+ this.connectionManager?.reportRequestFailure(error);
2036
+ throw error;
3009
2037
  }
3010
- async request(path, method, options) {
3011
- const effectiveHeaders = {
3012
- ...options?.headers,
3013
- ...this.authStrategy.getHeaders()
3014
- };
3015
- try {
3016
- const result = await request({
3017
- path,
3018
- method,
3019
- body: options?.body,
3020
- baseUrl: this.baseUrl,
3021
- extraHeaders: effectiveHeaders,
3022
- raw: options?.raw
3023
- });
3024
- this.connectionManager?.reportRequestSuccess();
3025
- return result;
3026
- } catch (error) {
3027
- this.connectionManager?.reportRequestFailure(error);
3028
- throw error;
3029
- }
3030
- }
3031
- async requestGameBackend(path, method, body, headers, raw) {
3032
- const effectiveHeaders = {
3033
- ...headers,
3034
- ...this.authStrategy.getHeaders()
3035
- };
3036
- try {
3037
- const result = await request({
3038
- path,
3039
- method,
3040
- body,
3041
- baseUrl: this.getGameBackendUrl(),
3042
- extraHeaders: effectiveHeaders,
3043
- raw
3044
- });
3045
- this.connectionManager?.reportRequestSuccess();
3046
- return result;
3047
- } catch (error) {
3048
- this.connectionManager?.reportRequestFailure(error);
3049
- throw error;
3050
- }
3051
- }
3052
- _ensureGameId() {
3053
- if (!this.gameId) {
3054
- throw new PlaycademyError("This operation requires a gameId, but none was provided when initializing the client.");
3055
- }
3056
- return this.gameId;
3057
- }
3058
- _detectAuthContext() {
3059
- this.authContext = { isInIframe: isInIframe() };
2038
+ }
2039
+ _ensureGameId() {
2040
+ if (!this.gameId) {
2041
+ throw new PlaycademyError("This operation requires a gameId, but none was provided when initializing the client.");
3060
2042
  }
3061
- _initializeConnectionMonitor() {
3062
- if (typeof window === "undefined")
3063
- return;
3064
- const isEnabled = this.config.enableConnectionMonitoring ?? true;
3065
- if (!isEnabled)
3066
- return;
3067
- try {
3068
- this.connectionManager = new ConnectionManager({
3069
- baseUrl: this.baseUrl,
3070
- authContext: this.authContext,
3071
- onDisconnect: this.config.onDisconnect,
3072
- onConnectionChange: (state, reason) => {
3073
- this.emit("connectionChange", { state, reason });
3074
- }
3075
- });
3076
- } catch (error) {
3077
- log.error("[Playcademy SDK] Failed to initialize connection manager:", { error });
3078
- }
2043
+ return this.gameId;
2044
+ }
2045
+ _detectAuthContext() {
2046
+ this.authContext = { isInIframe: isInIframe() };
2047
+ }
2048
+ _initializeConnectionMonitor() {
2049
+ if (typeof window === "undefined")
2050
+ return;
2051
+ const isEnabled = this.config.enableConnectionMonitoring ?? true;
2052
+ if (!isEnabled)
2053
+ return;
2054
+ try {
2055
+ this.connectionManager = new ConnectionManager({
2056
+ baseUrl: this.baseUrl,
2057
+ authContext: this.authContext,
2058
+ onDisconnect: this.config.onDisconnect,
2059
+ onConnectionChange: (state, reason) => {
2060
+ this.emit("connectionChange", { state, reason });
2061
+ }
2062
+ });
2063
+ } catch (error) {
2064
+ log.error("[Playcademy SDK] Failed to initialize connection manager:", { error });
3079
2065
  }
3080
- async _initializeInternalSession() {
3081
- if (!this.gameId || this.internalClientSessionId)
3082
- return;
3083
- const shouldAutoStart = this.config.autoStartSession ?? true;
3084
- if (!shouldAutoStart)
3085
- return;
3086
- try {
3087
- const response = await this.games.startSession(this.gameId);
3088
- this.internalClientSessionId = response.sessionId;
3089
- log.debug("[Playcademy SDK] Auto-started game session", {
3090
- gameId: this.gameId,
3091
- sessionId: this.internalClientSessionId
3092
- });
3093
- } catch (error) {
3094
- log.error("[Playcademy SDK] Auto-starting session failed for game", {
3095
- gameId: this.gameId,
3096
- error
3097
- });
3098
- }
2066
+ }
2067
+ async _initializeInternalSession() {
2068
+ if (!this.gameId || this.internalClientSessionId)
2069
+ return;
2070
+ const shouldAutoStart = this.config.autoStartSession ?? true;
2071
+ if (!shouldAutoStart)
2072
+ return;
2073
+ try {
2074
+ const response = await this._sessionManager.startSession(this.gameId);
2075
+ this.internalClientSessionId = response.sessionId;
2076
+ log.debug("[Playcademy SDK] Auto-started game session", {
2077
+ gameId: this.gameId,
2078
+ sessionId: this.internalClientSessionId
2079
+ });
2080
+ } catch (error) {
2081
+ log.error("[Playcademy SDK] Auto-starting session failed for game", {
2082
+ gameId: this.gameId,
2083
+ error
2084
+ });
3099
2085
  }
3100
- auth = createAuthNamespace(this);
3101
- identity = createIdentityNamespace(this);
3102
- runtime = createRuntimeNamespace(this);
3103
- games = createGamesNamespace(this);
3104
- users = createUsersNamespace(this);
3105
- dev = createDevNamespace(this);
3106
- maps = createMapsNamespace(this);
3107
- admin = createAdminNamespace(this);
3108
- shop = createShopNamespace(this);
3109
- levels = createLevelsNamespace(this);
3110
- timeback = createTimebackNamespace(this);
3111
- telemetry = createTelemetryNamespace(this);
3112
- credits = createCreditsNamespace(this);
3113
- leaderboard = createLeaderboardNamespace(this);
3114
- scores = createScoresNamespace(this);
3115
- character = createCharacterNamespace(this);
3116
- sprites = createSpritesNamespace(this);
3117
- realtime = createRealtimeNamespace(this);
3118
- achievements = createAchievementsNamespace(this);
3119
- notifications = createNotificationsNamespace(this);
3120
- backend = createBackendNamespace(this);
3121
- static init = init;
3122
- static login = login2;
3123
- static identity = identity;
3124
- };
3125
- });
3126
-
3127
- // src/index.ts
3128
- init_client();
3129
- init_errors();
3130
- init_connection();
3131
- init_messaging();
2086
+ }
2087
+ identity = createIdentityNamespace(this);
2088
+ runtime = createRuntimeNamespace(this);
2089
+ users = createUsersNamespace(this);
2090
+ timeback = createTimebackNamespace(this);
2091
+ credits = createCreditsNamespace(this);
2092
+ scores = createScoresNamespace(this);
2093
+ realtime = createRealtimeNamespace(this);
2094
+ backend = createBackendNamespace(this);
2095
+ static init = init;
2096
+ static login = login;
2097
+ static identity = identity;
2098
+ }
3132
2099
  export {
3133
2100
  messaging,
3134
2101
  extractApiErrorInfo,