@playcademy/sdk 0.3.6-beta.2 → 0.3.6-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -7,21 +7,6 @@ import { AUTH_PROVIDER_IDS } from '@playcademy/constants';
7
7
  declare class PlaycademyError extends Error {
8
8
  constructor(message: string);
9
9
  }
10
- type CorsProbeResult = 'reachable' | 'unreachable' | 'skipped';
11
- interface ManifestErrorDetails {
12
- manifestUrl: string;
13
- manifestHost: string;
14
- deploymentUrl: string;
15
- fetchOutcome: 'network_error' | 'bad_status' | 'invalid_body';
16
- retryCount: number;
17
- durationMs: number;
18
- fetchErrorMessage?: string;
19
- corsProbe?: CorsProbeResult;
20
- status?: number;
21
- contentType?: string;
22
- cfRay?: string;
23
- redirected?: boolean;
24
- }
25
10
  /**
26
11
  * Error codes returned by the API.
27
12
  * These map to specific error types and HTTP status codes.
@@ -2330,4 +2315,4 @@ declare class PlaycademyMessaging {
2330
2315
  declare const messaging: PlaycademyMessaging;
2331
2316
 
2332
2317
  export { ApiError, ConnectionManager, ConnectionMonitor, MessageEvents, PlaycademyClient, PlaycademyError, extractApiErrorInfo, messaging };
2333
- export type { ApiErrorCode, ApiErrorInfo, ConnectionMonitorConfig, ConnectionState, ConnectionStatePayload, DevUploadEvent, DevUploadHooks, DisconnectContext, DisconnectHandler, DisplayAlertPayload, ErrorResponseBody, ManifestErrorDetails };
2318
+ export type { ApiErrorCode, ApiErrorInfo, ConnectionMonitorConfig, ConnectionState, ConnectionStatePayload, DevUploadEvent, DevUploadHooks, DisconnectContext, DisconnectHandler, DisplayAlertPayload, ErrorResponseBody };
package/dist/index.js CHANGED
@@ -407,18 +407,6 @@ class PlaycademyError extends Error {
407
407
  }
408
408
  }
409
409
 
410
- class ManifestError extends PlaycademyError {
411
- kind;
412
- details;
413
- constructor(message, kind, details) {
414
- super(message);
415
- this.name = "ManifestError";
416
- this.kind = kind;
417
- this.details = details;
418
- Object.setPrototypeOf(this, ManifestError.prototype);
419
- }
420
- }
421
-
422
410
  class ApiError extends Error {
423
411
  code;
424
412
  details;
@@ -1972,98 +1960,6 @@ function unwrapFetchResult(result) {
1972
1960
  }
1973
1961
  return result.response;
1974
1962
  }
1975
- // src/core/transport/manifest.ts
1976
- var CORS_PROBE_TIMEOUT_MS = 3000;
1977
- async function probeCorsReachability(url) {
1978
- if (typeof globalThis.AbortController === "undefined") {
1979
- return "skipped";
1980
- }
1981
- const controller = new AbortController;
1982
- const timer = setTimeout(() => controller.abort(), CORS_PROBE_TIMEOUT_MS);
1983
- try {
1984
- await fetch(url, {
1985
- mode: "no-cors",
1986
- cache: "no-store",
1987
- signal: controller.signal
1988
- });
1989
- return "reachable";
1990
- } catch {
1991
- return "unreachable";
1992
- } finally {
1993
- clearTimeout(timer);
1994
- }
1995
- }
1996
- var MAX_FETCH_ERROR_MESSAGE_LENGTH = 512;
1997
- function getFetchErrorMessage(error) {
1998
- let raw;
1999
- if (error instanceof Error) {
2000
- raw = error.message;
2001
- } else if (typeof error === "string") {
2002
- raw = error;
2003
- }
2004
- if (!raw) {
2005
- return;
2006
- }
2007
- const normalized = raw.replace(/\s+/g, " ").trim();
2008
- if (!normalized) {
2009
- return;
2010
- }
2011
- return normalized.slice(0, MAX_FETCH_ERROR_MESSAGE_LENGTH);
2012
- }
2013
- async function fetchManifest(deploymentUrl) {
2014
- const manifestUrl = `${deploymentUrl.replace(/\/$/, "")}/playcademy.manifest.json`;
2015
- const result = await fetchWithRetry(manifestUrl, { method: "GET" });
2016
- let manifestHost;
2017
- try {
2018
- manifestHost = new URL(manifestUrl).host;
2019
- } catch {
2020
- manifestHost = manifestUrl;
2021
- }
2022
- const base = {
2023
- manifestUrl,
2024
- manifestHost,
2025
- deploymentUrl,
2026
- retryCount: result.retryCount,
2027
- durationMs: result.durationMs
2028
- };
2029
- if (result.error || !result.response) {
2030
- log.error(`[Playcademy SDK] Error fetching manifest from ${manifestUrl}:`, {
2031
- error: result.error
2032
- });
2033
- throw new ManifestError("Failed to load game manifest", "temporary", {
2034
- ...base,
2035
- fetchOutcome: "network_error",
2036
- ...result.error ? { fetchErrorMessage: getFetchErrorMessage(result.error) } : {}
2037
- });
2038
- }
2039
- const response = result.response;
2040
- if (!response.ok) {
2041
- log.error(`[fetchManifest] Failed to fetch manifest from ${manifestUrl}. Status: ${response.status}`);
2042
- throw new ManifestError(`Failed to fetch manifest: ${response.status} ${response.statusText}`, isRetryableStatus(response.status) ? "temporary" : "permanent", {
2043
- ...base,
2044
- fetchOutcome: "bad_status",
2045
- status: response.status,
2046
- contentType: response.headers.get("content-type") ?? undefined,
2047
- cfRay: response.headers.get("cf-ray") ?? undefined,
2048
- redirected: response.redirected
2049
- });
2050
- }
2051
- try {
2052
- return await response.json();
2053
- } catch (error) {
2054
- log.error(`[Playcademy SDK] Error parsing manifest from ${manifestUrl}:`, {
2055
- error
2056
- });
2057
- throw new ManifestError("Failed to parse game manifest", "permanent", {
2058
- ...base,
2059
- fetchOutcome: "invalid_body",
2060
- status: response.status,
2061
- contentType: response.headers.get("content-type") ?? undefined,
2062
- cfRay: response.headers.get("cf-ray") ?? undefined,
2063
- redirected: response.redirected
2064
- });
2065
- }
2066
- }
2067
1963
  // src/core/transport/request.ts
2068
1964
  function prepareRequestBody(body, headers) {
2069
1965
  if (body instanceof FormData) {
@@ -8,259 +8,6 @@ import { AUTH_PROVIDER_IDS } from '@playcademy/constants';
8
8
  /** Permitted HTTP verbs */
9
9
  type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
10
10
 
11
- /**
12
- * Game Types
13
- *
14
- * Literal types and API DTOs. Database row types are in @playcademy/data/types.
15
- *
16
- * @module types/game
17
- */
18
- type GameType = 'hosted' | 'external';
19
- type GamePlatform = 'web' | 'godot' | 'unity' | (string & {});
20
- /**
21
- * Game manifest file format (manifest.json).
22
- * Note: createdAt is a string here because it's parsed from JSON file.
23
- */
24
- interface ManifestV1 {
25
- version: string;
26
- platform: string;
27
- createdAt: string;
28
- }
29
- /** Log entry captured from seed worker console output */
30
- interface SeedLogEntry {
31
- /** Log level (log, warn, error, info) */
32
- level: 'log' | 'warn' | 'error' | 'info';
33
- /** Log message content */
34
- message: string;
35
- /** Milliseconds since seed execution started */
36
- timestamp: number;
37
- }
38
- /** Structured error details from D1/SQLite errors */
39
- interface SeedErrorDetails {
40
- /** Error category code */
41
- code?: 'CONSTRAINT_VIOLATION' | 'SQL_ERROR' | 'DATABASE_BUSY';
42
- /** Table name involved in the error */
43
- table?: string;
44
- /** Constraint name or column that caused the error */
45
- constraint?: string;
46
- /** Specific constraint type */
47
- constraintType?: 'UNIQUE' | 'FOREIGN_KEY' | 'NOT_NULL';
48
- /** Token near syntax error */
49
- nearToken?: string;
50
- /** Specific error type within category */
51
- errorType?: 'TABLE_NOT_FOUND' | 'SYNTAX_ERROR';
52
- }
53
- /**
54
- * API response for seed operations (what the API returns to the CLI).
55
- *
56
- * Extends the worker response with deployment metadata.
57
- */
58
- interface SeedResponse {
59
- /** Whether the seed completed successfully */
60
- success: boolean;
61
- /** Unique identifier for the seed worker deployment */
62
- deploymentId: string;
63
- /** When the seed was executed (ISO 8601 string from JSON serialization) */
64
- executedAt: string;
65
- /** Captured console output from the seed script */
66
- logs?: SeedLogEntry[];
67
- /** Execution duration in milliseconds */
68
- duration?: number;
69
- /** Error message if seed failed */
70
- error?: string;
71
- /** Stack trace if seed failed */
72
- stack?: string;
73
- /** Structured error details if seed failed */
74
- details?: SeedErrorDetails;
75
- }
76
- interface DomainValidationRecords {
77
- ownership?: {
78
- name?: string;
79
- value?: string;
80
- type?: string;
81
- };
82
- ssl?: {
83
- txt_name?: string;
84
- txt_value?: string;
85
- }[];
86
- }
87
-
88
- /**
89
- * Base error class for Cademy SDK specific errors.
90
- */
91
- declare class PlaycademyError extends Error {
92
- constructor(message: string);
93
- }
94
- type ManifestErrorKind = 'temporary' | 'permanent';
95
- type CorsProbeResult = 'reachable' | 'unreachable' | 'skipped';
96
- interface ManifestErrorDetails {
97
- manifestUrl: string;
98
- manifestHost: string;
99
- deploymentUrl: string;
100
- fetchOutcome: 'network_error' | 'bad_status' | 'invalid_body';
101
- retryCount: number;
102
- durationMs: number;
103
- fetchErrorMessage?: string;
104
- corsProbe?: CorsProbeResult;
105
- status?: number;
106
- contentType?: string;
107
- cfRay?: string;
108
- redirected?: boolean;
109
- }
110
- declare class ManifestError extends PlaycademyError {
111
- readonly kind: ManifestErrorKind;
112
- readonly details: ManifestErrorDetails | undefined;
113
- constructor(message: string, kind: ManifestErrorKind, details?: ManifestErrorDetails);
114
- }
115
- /**
116
- * Error codes returned by the API.
117
- * These map to specific error types and HTTP status codes.
118
- */
119
- type ApiErrorCode = 'BAD_REQUEST' | 'UNAUTHORIZED' | 'FORBIDDEN' | 'ACCESS_DENIED' | 'NOT_FOUND' | 'METHOD_NOT_ALLOWED' | 'CONFLICT' | 'ALREADY_EXISTS' | 'GONE' | 'PRECONDITION_FAILED' | 'PAYLOAD_TOO_LARGE' | 'VALIDATION_FAILED' | 'TOO_MANY_REQUESTS' | 'RATE_LIMITED' | 'EXPIRED' | 'INTERNAL' | 'INTERNAL_ERROR' | 'NOT_IMPLEMENTED' | 'SERVICE_UNAVAILABLE' | 'TIMEOUT' | string;
120
- /**
121
- * Structure of error response bodies returned by API endpoints.
122
- *
123
- * @example
124
- * ```json
125
- * {
126
- * "error": {
127
- * "code": "NOT_FOUND",
128
- * "message": "Item not found",
129
- * "details": { "identifier": "abc123" }
130
- * }
131
- * }
132
- * ```
133
- */
134
- interface ErrorResponseBody {
135
- error?: {
136
- code?: string;
137
- message?: string;
138
- details?: unknown;
139
- };
140
- }
141
- /**
142
- * API error thrown when a request fails.
143
- *
144
- * Contains structured error information from the API response:
145
- * - `status` - HTTP status code (e.g., 404)
146
- * - `code` - API error code (e.g., "NOT_FOUND")
147
- * - `message` - Human-readable error message
148
- * - `details` - Optional additional error context
149
- *
150
- * @example
151
- * ```typescript
152
- * try {
153
- * await client.games.get('nonexistent')
154
- * } catch (error) {
155
- * if (error instanceof ApiError) {
156
- * console.log(error.status) // 404
157
- * console.log(error.code) // "NOT_FOUND"
158
- * console.log(error.message) // "Game not found"
159
- * console.log(error.details) // { identifier: "nonexistent" }
160
- * }
161
- * }
162
- * ```
163
- */
164
- declare class ApiError extends Error {
165
- /**
166
- * API error code (e.g., "NOT_FOUND", "VALIDATION_FAILED").
167
- * Use this for programmatic error handling.
168
- */
169
- readonly code: ApiErrorCode;
170
- /**
171
- * Additional error context from the API.
172
- * Structure varies by error type (e.g., validation errors include field details).
173
- */
174
- readonly details: unknown;
175
- /**
176
- * Raw response body for debugging.
177
- * @internal
178
- */
179
- readonly rawBody: unknown;
180
- readonly status: number;
181
- constructor(
182
- /** HTTP status code */
183
- status: number,
184
- /** API error code */
185
- code: ApiErrorCode,
186
- /** Human-readable error message */
187
- message: string,
188
- /** Additional error context */
189
- details?: unknown,
190
- /** Raw response body */
191
- rawBody?: unknown);
192
- /**
193
- * Create an ApiError from an HTTP response.
194
- * Parses the structured error response from the API.
195
- *
196
- * @internal
197
- */
198
- static fromResponse(status: number, statusText: string, body: unknown): ApiError;
199
- /**
200
- * Check if this is a specific error type.
201
- *
202
- * @example
203
- * ```typescript
204
- * if (error.is('NOT_FOUND')) {
205
- * // Handle not found
206
- * } else if (error.is('VALIDATION_FAILED')) {
207
- * // Handle validation error
208
- * }
209
- * ```
210
- */
211
- is(code: ApiErrorCode): boolean;
212
- /**
213
- * Check if this is a client error (4xx).
214
- */
215
- isClientError(): boolean;
216
- /**
217
- * Check if this is a server error (5xx).
218
- */
219
- isServerError(): boolean;
220
- /**
221
- * Check if this error is retryable.
222
- * Server errors and rate limits are typically retryable.
223
- */
224
- isRetryable(): boolean;
225
- }
226
- /**
227
- * Extracted error information for display purposes.
228
- */
229
- interface ApiErrorInfo {
230
- /** HTTP status code */
231
- status: number;
232
- /** API error code */
233
- code: ApiErrorCode;
234
- /** Human-readable error message */
235
- message: string;
236
- /** Additional error context */
237
- details?: unknown;
238
- }
239
- /**
240
- * Extract useful error information from an API error.
241
- * Useful for displaying errors to users in a friendly way.
242
- *
243
- * @example
244
- * ```typescript
245
- * try {
246
- * await client.shop.purchase(itemId)
247
- * } catch (error) {
248
- * const info = extractApiErrorInfo(error)
249
- * if (info) {
250
- * showToast(`Error: ${info.message}`)
251
- * }
252
- * }
253
- * ```
254
- */
255
- declare function extractApiErrorInfo(error: unknown): ApiErrorInfo | null;
256
-
257
- /**
258
- * Fire-and-forget reachability check using `no-cors` mode.
259
- * Exported for use by the app telemetry layer — not awaited inside
260
- * fetchManifest so it doesn't block the error screen.
261
- */
262
- declare function probeCorsReachability(url: string): Promise<CorsProbeResult>;
263
-
264
11
  /**
265
12
  * User Types
266
13
  *
@@ -341,6 +88,83 @@ interface GameUser {
341
88
  timeback?: UserTimebackData;
342
89
  }
343
90
 
91
+ /**
92
+ * Game Types
93
+ *
94
+ * Literal types and API DTOs. Database row types are in @playcademy/data/types.
95
+ *
96
+ * @module types/game
97
+ */
98
+ type GameType = 'hosted' | 'external';
99
+ type GamePlatform = 'web' | 'godot' | 'unity' | (string & {});
100
+ /**
101
+ * Game manifest file format (manifest.json).
102
+ * Note: createdAt is a string here because it's parsed from JSON file.
103
+ */
104
+ interface ManifestV1 {
105
+ version: string;
106
+ platform: string;
107
+ createdAt: string;
108
+ }
109
+ /** Log entry captured from seed worker console output */
110
+ interface SeedLogEntry {
111
+ /** Log level (log, warn, error, info) */
112
+ level: 'log' | 'warn' | 'error' | 'info';
113
+ /** Log message content */
114
+ message: string;
115
+ /** Milliseconds since seed execution started */
116
+ timestamp: number;
117
+ }
118
+ /** Structured error details from D1/SQLite errors */
119
+ interface SeedErrorDetails {
120
+ /** Error category code */
121
+ code?: 'CONSTRAINT_VIOLATION' | 'SQL_ERROR' | 'DATABASE_BUSY';
122
+ /** Table name involved in the error */
123
+ table?: string;
124
+ /** Constraint name or column that caused the error */
125
+ constraint?: string;
126
+ /** Specific constraint type */
127
+ constraintType?: 'UNIQUE' | 'FOREIGN_KEY' | 'NOT_NULL';
128
+ /** Token near syntax error */
129
+ nearToken?: string;
130
+ /** Specific error type within category */
131
+ errorType?: 'TABLE_NOT_FOUND' | 'SYNTAX_ERROR';
132
+ }
133
+ /**
134
+ * API response for seed operations (what the API returns to the CLI).
135
+ *
136
+ * Extends the worker response with deployment metadata.
137
+ */
138
+ interface SeedResponse {
139
+ /** Whether the seed completed successfully */
140
+ success: boolean;
141
+ /** Unique identifier for the seed worker deployment */
142
+ deploymentId: string;
143
+ /** When the seed was executed (ISO 8601 string from JSON serialization) */
144
+ executedAt: string;
145
+ /** Captured console output from the seed script */
146
+ logs?: SeedLogEntry[];
147
+ /** Execution duration in milliseconds */
148
+ duration?: number;
149
+ /** Error message if seed failed */
150
+ error?: string;
151
+ /** Stack trace if seed failed */
152
+ stack?: string;
153
+ /** Structured error details if seed failed */
154
+ details?: SeedErrorDetails;
155
+ }
156
+ interface DomainValidationRecords {
157
+ ownership?: {
158
+ name?: string;
159
+ value?: string;
160
+ type?: string;
161
+ };
162
+ ssl?: {
163
+ txt_name?: string;
164
+ txt_value?: string;
165
+ }[];
166
+ }
167
+
344
168
  /**
345
169
  * Leaderboard Types
346
170
  *
@@ -915,7 +739,7 @@ interface TimebackStudentCourseOverview {
915
739
  completionStatus: CourseCompletionStatus;
916
740
  history: TimebackStudentHistoryPoint[];
917
741
  }
918
- type TimebackRecentActivityKind = 'activity' | 'time-spent' | 'remediation-xp' | 'remediation-time' | 'remediation-mastery' | 'admin-completion';
742
+ type TimebackRecentActivityKind = 'activity' | 'time-spent' | 'remediation-xp' | 'remediation-time' | 'remediation-mastery' | 'course-completed' | 'course-resumed';
919
743
  interface TimebackRecentActivity {
920
744
  id: string;
921
745
  kind: TimebackRecentActivityKind;
@@ -1222,6 +1046,154 @@ interface SpriteConfigWithDimensions {
1222
1046
  animations: Record<string, SpriteAnimationFrame>;
1223
1047
  }
1224
1048
 
1049
+ /**
1050
+ * Base error class for Cademy SDK specific errors.
1051
+ */
1052
+ declare class PlaycademyError extends Error {
1053
+ constructor(message: string);
1054
+ }
1055
+ /**
1056
+ * Error codes returned by the API.
1057
+ * These map to specific error types and HTTP status codes.
1058
+ */
1059
+ type ApiErrorCode = 'BAD_REQUEST' | 'UNAUTHORIZED' | 'FORBIDDEN' | 'ACCESS_DENIED' | 'NOT_FOUND' | 'METHOD_NOT_ALLOWED' | 'CONFLICT' | 'ALREADY_EXISTS' | 'GONE' | 'PRECONDITION_FAILED' | 'PAYLOAD_TOO_LARGE' | 'VALIDATION_FAILED' | 'TOO_MANY_REQUESTS' | 'RATE_LIMITED' | 'EXPIRED' | 'INTERNAL' | 'INTERNAL_ERROR' | 'NOT_IMPLEMENTED' | 'SERVICE_UNAVAILABLE' | 'TIMEOUT' | string;
1060
+ /**
1061
+ * Structure of error response bodies returned by API endpoints.
1062
+ *
1063
+ * @example
1064
+ * ```json
1065
+ * {
1066
+ * "error": {
1067
+ * "code": "NOT_FOUND",
1068
+ * "message": "Item not found",
1069
+ * "details": { "identifier": "abc123" }
1070
+ * }
1071
+ * }
1072
+ * ```
1073
+ */
1074
+ interface ErrorResponseBody {
1075
+ error?: {
1076
+ code?: string;
1077
+ message?: string;
1078
+ details?: unknown;
1079
+ };
1080
+ }
1081
+ /**
1082
+ * API error thrown when a request fails.
1083
+ *
1084
+ * Contains structured error information from the API response:
1085
+ * - `status` - HTTP status code (e.g., 404)
1086
+ * - `code` - API error code (e.g., "NOT_FOUND")
1087
+ * - `message` - Human-readable error message
1088
+ * - `details` - Optional additional error context
1089
+ *
1090
+ * @example
1091
+ * ```typescript
1092
+ * try {
1093
+ * await client.games.get('nonexistent')
1094
+ * } catch (error) {
1095
+ * if (error instanceof ApiError) {
1096
+ * console.log(error.status) // 404
1097
+ * console.log(error.code) // "NOT_FOUND"
1098
+ * console.log(error.message) // "Game not found"
1099
+ * console.log(error.details) // { identifier: "nonexistent" }
1100
+ * }
1101
+ * }
1102
+ * ```
1103
+ */
1104
+ declare class ApiError extends Error {
1105
+ /**
1106
+ * API error code (e.g., "NOT_FOUND", "VALIDATION_FAILED").
1107
+ * Use this for programmatic error handling.
1108
+ */
1109
+ readonly code: ApiErrorCode;
1110
+ /**
1111
+ * Additional error context from the API.
1112
+ * Structure varies by error type (e.g., validation errors include field details).
1113
+ */
1114
+ readonly details: unknown;
1115
+ /**
1116
+ * Raw response body for debugging.
1117
+ * @internal
1118
+ */
1119
+ readonly rawBody: unknown;
1120
+ readonly status: number;
1121
+ constructor(
1122
+ /** HTTP status code */
1123
+ status: number,
1124
+ /** API error code */
1125
+ code: ApiErrorCode,
1126
+ /** Human-readable error message */
1127
+ message: string,
1128
+ /** Additional error context */
1129
+ details?: unknown,
1130
+ /** Raw response body */
1131
+ rawBody?: unknown);
1132
+ /**
1133
+ * Create an ApiError from an HTTP response.
1134
+ * Parses the structured error response from the API.
1135
+ *
1136
+ * @internal
1137
+ */
1138
+ static fromResponse(status: number, statusText: string, body: unknown): ApiError;
1139
+ /**
1140
+ * Check if this is a specific error type.
1141
+ *
1142
+ * @example
1143
+ * ```typescript
1144
+ * if (error.is('NOT_FOUND')) {
1145
+ * // Handle not found
1146
+ * } else if (error.is('VALIDATION_FAILED')) {
1147
+ * // Handle validation error
1148
+ * }
1149
+ * ```
1150
+ */
1151
+ is(code: ApiErrorCode): boolean;
1152
+ /**
1153
+ * Check if this is a client error (4xx).
1154
+ */
1155
+ isClientError(): boolean;
1156
+ /**
1157
+ * Check if this is a server error (5xx).
1158
+ */
1159
+ isServerError(): boolean;
1160
+ /**
1161
+ * Check if this error is retryable.
1162
+ * Server errors and rate limits are typically retryable.
1163
+ */
1164
+ isRetryable(): boolean;
1165
+ }
1166
+ /**
1167
+ * Extracted error information for display purposes.
1168
+ */
1169
+ interface ApiErrorInfo {
1170
+ /** HTTP status code */
1171
+ status: number;
1172
+ /** API error code */
1173
+ code: ApiErrorCode;
1174
+ /** Human-readable error message */
1175
+ message: string;
1176
+ /** Additional error context */
1177
+ details?: unknown;
1178
+ }
1179
+ /**
1180
+ * Extract useful error information from an API error.
1181
+ * Useful for displaying errors to users in a friendly way.
1182
+ *
1183
+ * @example
1184
+ * ```typescript
1185
+ * try {
1186
+ * await client.shop.purchase(itemId)
1187
+ * } catch (error) {
1188
+ * const info = extractApiErrorInfo(error)
1189
+ * if (info) {
1190
+ * showToast(`Error: ${info.message}`)
1191
+ * }
1192
+ * }
1193
+ * ```
1194
+ */
1195
+ declare function extractApiErrorInfo(error: unknown): ApiErrorInfo | null;
1196
+
1225
1197
  /**
1226
1198
  * Connection monitoring types
1227
1199
  *
@@ -7787,5 +7759,5 @@ declare class PlaycademyInternalClient extends PlaycademyBaseClient {
7787
7759
  };
7788
7760
  }
7789
7761
 
7790
- export { AchievementCompletionType, ApiError, ConnectionManager, ConnectionMonitor, ManifestError, MessageEvents, NotificationStatus, NotificationType, PlaycademyInternalClient as PlaycademyClient, PlaycademyError, PlaycademyInternalClient, extractApiErrorInfo, messaging, probeCorsReachability };
7791
- export type { AchievementCurrent, AchievementHistoryEntry, AchievementProgressResponse, AchievementScopeType, AchievementWithStatus, ApiErrorCode, ApiErrorInfo, AuthCallbackPayload, AuthOptions, AuthProviderType, AuthResult, AuthServerMessage, AuthStateChangePayload, AuthStateUpdate, AuthenticatedUser, BetterAuthApiKey, BetterAuthApiKeyResponse, BetterAuthSignInResponse, BucketFile, CharacterComponentRow as CharacterComponent, CharacterComponentType, CharacterComponentWithSpriteUrl, CharacterComponentsOptions, ClientConfig, ClientEvents, ConnectionMonitorConfig, ConnectionState, ConnectionStatePayload, CorsProbeResult, CourseXp, CreateCharacterData, CreateMapObjectData, CurrencyRow as Currency, DevUploadEvent, DevUploadHooks, DeveloperStatusEnumType, DeveloperStatusResponse, DeveloperStatusValue, DisconnectContext, DisconnectHandler, DisplayAlertPayload, ErrorResponseBody, EventListeners, ExternalGame, FetchedGame, Game, GameContextPayload, GameCustomHostname, GameInitUser, GameLeaderboardEntry, MapRow as GameMap, GamePlatform, GameRow as GameRecord, GameSessionRow as GameSession, GameTimebackIntegration, GameTokenResponse, GameType, GameUser, GetXpOptions, HostedGame, InitPayload, InsertCurrencyInput, InsertItemInput, InsertShopListingInput, InteractionType, InventoryItemRow as InventoryItem, InventoryItemWithItem, InventoryMutationResponse, ItemRow as Item, ItemType, KVKeyEntry, KVKeyMetadata, KVSeedEntry, KVStatsResponse, KeyEventPayload, LeaderboardEntry, LeaderboardOptions, LeaderboardTimeframe, LevelConfigRow as LevelConfig, LevelProgressResponse, LevelUpCheckResult, LoginResponse, ManifestErrorDetails, ManifestV1, MapData, MapElementRow as MapElement, MapElementMetadata, MapElementWithGame, MapObjectRow as MapObject, MapObjectWithItem, NotificationRow as Notification, NotificationStats, PlaceableItemMetadata, PlatformTimebackUser, PlatformTimebackUserContext, PlaycademyServerClientConfig, PlaycademyServerClientState, PlayerCharacterRow as PlayerCharacter, PlayerCharacterAccessoryRow as PlayerCharacterAccessory, PlayerCurrency, PlayerInventoryItem, PlayerProfile, PlayerSessionPayload, PopulateStudentResponse, RealtimeTokenResponse, ScoreSubmission, ShopCurrency, ShopDisplayItem, ShopListingRow as ShopListing, ShopViewResponse, SpriteAnimationFrame, SpriteConfigWithDimensions, SpriteTemplateRow as SpriteTemplate, SpriteTemplateData, StartSessionResponse, TelemetryPayload, TimebackEnrollment, TimebackInitContext, TimebackOrganization, TimebackUser, TimebackUserContext, TimebackUserXp, TodayXpResponse, TokenRefreshPayload, TokenType, TotalXpResponse, UpdateCharacterData, UpdateCurrencyInput, UpdateItemInput, UpdateShopListingInput, UpsertGameMetadataInput, UserRow as User, UserEnrollment, UserInfo, UserLevelRow as UserLevel, UserLevelWithConfig, UserOrganization, UserRank, UserRankResponse, UserRoleEnumType, UserScore, UserTimebackData, XPAddResult, XpHistoryResponse, XpResponse, XpSummaryResponse };
7762
+ export { AchievementCompletionType, ApiError, ConnectionManager, ConnectionMonitor, MessageEvents, NotificationStatus, NotificationType, PlaycademyInternalClient as PlaycademyClient, PlaycademyError, PlaycademyInternalClient, extractApiErrorInfo, messaging };
7763
+ export type { AchievementCurrent, AchievementHistoryEntry, AchievementProgressResponse, AchievementScopeType, AchievementWithStatus, ApiErrorCode, ApiErrorInfo, AuthCallbackPayload, AuthOptions, AuthProviderType, AuthResult, AuthServerMessage, AuthStateChangePayload, AuthStateUpdate, AuthenticatedUser, BetterAuthApiKey, BetterAuthApiKeyResponse, BetterAuthSignInResponse, BucketFile, CharacterComponentRow as CharacterComponent, CharacterComponentType, CharacterComponentWithSpriteUrl, CharacterComponentsOptions, ClientConfig, ClientEvents, ConnectionMonitorConfig, ConnectionState, ConnectionStatePayload, CourseXp, CreateCharacterData, CreateMapObjectData, CurrencyRow as Currency, DevUploadEvent, DevUploadHooks, DeveloperStatusEnumType, DeveloperStatusResponse, DeveloperStatusValue, DisconnectContext, DisconnectHandler, DisplayAlertPayload, ErrorResponseBody, EventListeners, ExternalGame, FetchedGame, Game, GameContextPayload, GameCustomHostname, GameInitUser, GameLeaderboardEntry, MapRow as GameMap, GamePlatform, GameRow as GameRecord, GameSessionRow as GameSession, GameTimebackIntegration, GameTokenResponse, GameType, GameUser, GetXpOptions, HostedGame, InitPayload, InsertCurrencyInput, InsertItemInput, InsertShopListingInput, InteractionType, InventoryItemRow as InventoryItem, InventoryItemWithItem, InventoryMutationResponse, ItemRow as Item, ItemType, KVKeyEntry, KVKeyMetadata, KVSeedEntry, KVStatsResponse, KeyEventPayload, LeaderboardEntry, LeaderboardOptions, LeaderboardTimeframe, LevelConfigRow as LevelConfig, LevelProgressResponse, LevelUpCheckResult, LoginResponse, ManifestV1, MapData, MapElementRow as MapElement, MapElementMetadata, MapElementWithGame, MapObjectRow as MapObject, MapObjectWithItem, NotificationRow as Notification, NotificationStats, PlaceableItemMetadata, PlatformTimebackUser, PlatformTimebackUserContext, PlaycademyServerClientConfig, PlaycademyServerClientState, PlayerCharacterRow as PlayerCharacter, PlayerCharacterAccessoryRow as PlayerCharacterAccessory, PlayerCurrency, PlayerInventoryItem, PlayerProfile, PlayerSessionPayload, PopulateStudentResponse, RealtimeTokenResponse, ScoreSubmission, ShopCurrency, ShopDisplayItem, ShopListingRow as ShopListing, ShopViewResponse, SpriteAnimationFrame, SpriteConfigWithDimensions, SpriteTemplateRow as SpriteTemplate, SpriteTemplateData, StartSessionResponse, TelemetryPayload, TimebackEnrollment, TimebackInitContext, TimebackOrganization, TimebackUser, TimebackUserContext, TimebackUserXp, TodayXpResponse, TokenRefreshPayload, TokenType, TotalXpResponse, UpdateCharacterData, UpdateCurrencyInput, UpdateItemInput, UpdateShopListingInput, UpsertGameMetadataInput, UserRow as User, UserEnrollment, UserInfo, UserLevelRow as UserLevel, UserLevelWithConfig, UserOrganization, UserRank, UserRankResponse, UserRoleEnumType, UserScore, UserTimebackData, XPAddResult, XpHistoryResponse, XpResponse, XpSummaryResponse };
package/dist/internal.js CHANGED
@@ -407,18 +407,6 @@ class PlaycademyError extends Error {
407
407
  }
408
408
  }
409
409
 
410
- class ManifestError extends PlaycademyError {
411
- kind;
412
- details;
413
- constructor(message, kind, details) {
414
- super(message);
415
- this.name = "ManifestError";
416
- this.kind = kind;
417
- this.details = details;
418
- Object.setPrototypeOf(this, ManifestError.prototype);
419
- }
420
- }
421
-
422
410
  class ApiError extends Error {
423
411
  code;
424
412
  details;
@@ -2039,235 +2027,6 @@ function createDevNamespace(client) {
2039
2027
  }
2040
2028
  };
2041
2029
  }
2042
- // src/core/transport/retry.ts
2043
- var RETRY_DELAYS_MS = [500, 1500];
2044
- function wait(ms) {
2045
- return new Promise((resolve) => setTimeout(resolve, ms));
2046
- }
2047
- var retryRuntime = { wait };
2048
- function isRetryableStatus(status) {
2049
- return status === 429 || status >= 500;
2050
- }
2051
- async function fetchWithRetry(url, init2) {
2052
- const startedAt = Date.now();
2053
- for (let attempt = 0;attempt <= RETRY_DELAYS_MS.length; attempt++) {
2054
- const retryDelayMs = RETRY_DELAYS_MS[attempt];
2055
- const canRetry = init2.method === "GET" && retryDelayMs !== undefined;
2056
- try {
2057
- const response = await fetch(url, init2);
2058
- if (canRetry && isRetryableStatus(response.status)) {
2059
- await retryRuntime.wait(retryDelayMs);
2060
- } else {
2061
- return { response, retryCount: attempt, durationMs: Date.now() - startedAt };
2062
- }
2063
- } catch (error) {
2064
- if (canRetry && error instanceof TypeError) {
2065
- await retryRuntime.wait(retryDelayMs);
2066
- } else {
2067
- return { error, retryCount: attempt, durationMs: Date.now() - startedAt };
2068
- }
2069
- }
2070
- }
2071
- return {
2072
- error: new PlaycademyError("Request failed after exhausting retries"),
2073
- retryCount: RETRY_DELAYS_MS.length,
2074
- durationMs: Date.now() - startedAt
2075
- };
2076
- }
2077
- function unwrapFetchResult(result) {
2078
- if (result.error) {
2079
- throw result.error;
2080
- }
2081
- if (!result.response) {
2082
- throw new PlaycademyError("Request failed after exhausting retries");
2083
- }
2084
- return result.response;
2085
- }
2086
- // src/core/transport/manifest.ts
2087
- var CORS_PROBE_TIMEOUT_MS = 3000;
2088
- async function probeCorsReachability(url) {
2089
- if (typeof globalThis.AbortController === "undefined") {
2090
- return "skipped";
2091
- }
2092
- const controller = new AbortController;
2093
- const timer = setTimeout(() => controller.abort(), CORS_PROBE_TIMEOUT_MS);
2094
- try {
2095
- await fetch(url, {
2096
- mode: "no-cors",
2097
- cache: "no-store",
2098
- signal: controller.signal
2099
- });
2100
- return "reachable";
2101
- } catch {
2102
- return "unreachable";
2103
- } finally {
2104
- clearTimeout(timer);
2105
- }
2106
- }
2107
- var MAX_FETCH_ERROR_MESSAGE_LENGTH = 512;
2108
- function getFetchErrorMessage(error) {
2109
- let raw;
2110
- if (error instanceof Error) {
2111
- raw = error.message;
2112
- } else if (typeof error === "string") {
2113
- raw = error;
2114
- }
2115
- if (!raw) {
2116
- return;
2117
- }
2118
- const normalized = raw.replace(/\s+/g, " ").trim();
2119
- if (!normalized) {
2120
- return;
2121
- }
2122
- return normalized.slice(0, MAX_FETCH_ERROR_MESSAGE_LENGTH);
2123
- }
2124
- async function fetchManifest(deploymentUrl) {
2125
- const manifestUrl = `${deploymentUrl.replace(/\/$/, "")}/playcademy.manifest.json`;
2126
- const result = await fetchWithRetry(manifestUrl, { method: "GET" });
2127
- let manifestHost;
2128
- try {
2129
- manifestHost = new URL(manifestUrl).host;
2130
- } catch {
2131
- manifestHost = manifestUrl;
2132
- }
2133
- const base = {
2134
- manifestUrl,
2135
- manifestHost,
2136
- deploymentUrl,
2137
- retryCount: result.retryCount,
2138
- durationMs: result.durationMs
2139
- };
2140
- if (result.error || !result.response) {
2141
- log.error(`[Playcademy SDK] Error fetching manifest from ${manifestUrl}:`, {
2142
- error: result.error
2143
- });
2144
- throw new ManifestError("Failed to load game manifest", "temporary", {
2145
- ...base,
2146
- fetchOutcome: "network_error",
2147
- ...result.error ? { fetchErrorMessage: getFetchErrorMessage(result.error) } : {}
2148
- });
2149
- }
2150
- const response = result.response;
2151
- if (!response.ok) {
2152
- log.error(`[fetchManifest] Failed to fetch manifest from ${manifestUrl}. Status: ${response.status}`);
2153
- throw new ManifestError(`Failed to fetch manifest: ${response.status} ${response.statusText}`, isRetryableStatus(response.status) ? "temporary" : "permanent", {
2154
- ...base,
2155
- fetchOutcome: "bad_status",
2156
- status: response.status,
2157
- contentType: response.headers.get("content-type") ?? undefined,
2158
- cfRay: response.headers.get("cf-ray") ?? undefined,
2159
- redirected: response.redirected
2160
- });
2161
- }
2162
- try {
2163
- return await response.json();
2164
- } catch (error) {
2165
- log.error(`[Playcademy SDK] Error parsing manifest from ${manifestUrl}:`, {
2166
- error
2167
- });
2168
- throw new ManifestError("Failed to parse game manifest", "permanent", {
2169
- ...base,
2170
- fetchOutcome: "invalid_body",
2171
- status: response.status,
2172
- contentType: response.headers.get("content-type") ?? undefined,
2173
- cfRay: response.headers.get("cf-ray") ?? undefined,
2174
- redirected: response.redirected
2175
- });
2176
- }
2177
- }
2178
- // src/core/transport/request.ts
2179
- function prepareRequestBody(body, headers) {
2180
- if (body instanceof FormData) {
2181
- return body;
2182
- }
2183
- if (body instanceof ArrayBuffer || body instanceof Blob || ArrayBuffer.isView(body)) {
2184
- if (!headers["Content-Type"]) {
2185
- headers["Content-Type"] = "application/octet-stream";
2186
- }
2187
- return body;
2188
- }
2189
- if (body !== undefined && body !== null) {
2190
- if (headers["Content-Type"]) {
2191
- return typeof body === "string" ? body : JSON.stringify(body);
2192
- }
2193
- headers["Content-Type"] = "application/json";
2194
- return JSON.stringify(body);
2195
- }
2196
- return;
2197
- }
2198
- function checkDevWarnings(data) {
2199
- if (!data || typeof data !== "object") {
2200
- return;
2201
- }
2202
- const response = data;
2203
- const warningType = response.__playcademyDevWarning;
2204
- if (!warningType) {
2205
- return;
2206
- }
2207
- switch (warningType) {
2208
- case "timeback-not-configured": {
2209
- console.warn("%c⚠️ TimeBack Not Configured", "background: #f59e0b; color: white; padding: 6px 12px; border-radius: 4px; font-weight: bold; font-size: 13px");
2210
- console.log("%cTimeBack is configured in playcademy.config.js but the sandbox does not have TimeBack credentials.", "color: #f59e0b; font-weight: 500");
2211
- console.log("To test TimeBack locally:");
2212
- console.log(" Set the following environment variables:");
2213
- console.log(" • %cTIMEBACK_ONEROSTER_API_URL", "color: #0ea5e9; font-weight: 600; font-family: monospace");
2214
- console.log(" • %cTIMEBACK_CALIPER_API_URL", "color: #0ea5e9; font-weight: 600; font-family: monospace");
2215
- console.log(" • %cTIMEBACK_API_CLIENT_ID/SECRET", "color: #0ea5e9; font-weight: 600; font-family: monospace");
2216
- console.log(" Or deploy your game: %cplaycademy deploy", "color: #10b981; font-weight: 600; font-family: monospace");
2217
- console.log(" Or wait for %c@superbuilders/timeback-local%c (coming soon)", "color: #8b5cf6; font-weight: 600; font-family: monospace", "color: inherit");
2218
- break;
2219
- }
2220
- default: {
2221
- console.warn(`[Playcademy Dev Warning] ${warningType}`);
2222
- }
2223
- }
2224
- }
2225
- async function request({
2226
- path,
2227
- baseUrl,
2228
- method = "GET",
2229
- body,
2230
- extraHeaders = {},
2231
- raw = false
2232
- }) {
2233
- const url = baseUrl.replace(/\/$/, "") + (path.startsWith("/") ? path : `/${path}`);
2234
- const headers = { ...extraHeaders };
2235
- const payload = prepareRequestBody(body, headers);
2236
- const res = unwrapFetchResult(await fetchWithRetry(url, {
2237
- method,
2238
- headers,
2239
- body: payload,
2240
- credentials: "omit"
2241
- }));
2242
- if (raw) {
2243
- return res;
2244
- }
2245
- if (!res.ok) {
2246
- const clonedRes = res.clone();
2247
- const errorBody = await clonedRes.json().catch(() => clonedRes.text().catch(() => {
2248
- return;
2249
- })) ?? undefined;
2250
- throw ApiError.fromResponse(res.status, res.statusText, errorBody);
2251
- }
2252
- if (res.status === 204) {
2253
- return;
2254
- }
2255
- const contentType = res.headers.get("content-type") ?? "";
2256
- if (contentType.includes("application/json")) {
2257
- try {
2258
- const parsed = await res.json();
2259
- checkDevWarnings(parsed);
2260
- return parsed;
2261
- } catch (error) {
2262
- if (error instanceof SyntaxError) {
2263
- return;
2264
- }
2265
- throw error;
2266
- }
2267
- }
2268
- const rawText = await res.text().catch(() => "");
2269
- return rawText && rawText.length > 0 ? rawText : undefined;
2270
- }
2271
2030
  // src/namespaces/platform/games.ts
2272
2031
  function createGamesNamespace(client) {
2273
2032
  const gamesListCache = createTTLCache({
@@ -2284,7 +2043,7 @@ function createGamesNamespace(client) {
2284
2043
  return gameFetchCache.get(gameIdOrSlug, async () => {
2285
2044
  const baseGameData = await promise;
2286
2045
  if (baseGameData.gameType === "hosted" && baseGameData.deploymentUrl !== null && baseGameData.deploymentUrl !== undefined && baseGameData.deploymentUrl !== "") {
2287
- const manifestData = await fetchManifest(baseGameData.deploymentUrl);
2046
+ const manifestData = await client["request"](`/games/${baseGameData.id}/manifest`, "GET");
2288
2047
  return { ...baseGameData, manifest: manifestData };
2289
2048
  }
2290
2049
  return baseGameData;
@@ -2709,8 +2468,8 @@ function createTimebackNamespace2(client) {
2709
2468
  throw new Error(NOT_SUPPORTED);
2710
2469
  },
2711
2470
  management: {
2712
- setup: (request2) => client["request"]("/timeback/setup", "POST", {
2713
- body: request2
2471
+ setup: (request) => client["request"]("/timeback/setup", "POST", {
2472
+ body: request
2714
2473
  }),
2715
2474
  verify: (gameId) => client["request"](`/timeback/verify/${gameId}`, "GET"),
2716
2475
  cleanup: (gameId) => client["request"](`/timeback/integrations/${gameId}`, "DELETE"),
@@ -2786,17 +2545,17 @@ function createTimebackNamespace2(client) {
2786
2545
  }
2787
2546
  return client["request"](`/timeback/student-activity/${timebackId}/${courseId}?${params}`, "GET");
2788
2547
  },
2789
- grantXp: (request2) => client["request"]("/timeback/grant-xp", "POST", {
2790
- body: request2
2548
+ grantXp: (request) => client["request"]("/timeback/grant-xp", "POST", {
2549
+ body: request
2791
2550
  }),
2792
- adjustTime: (request2) => client["request"]("/timeback/adjust-time", "POST", {
2793
- body: request2
2551
+ adjustTime: (request) => client["request"]("/timeback/adjust-time", "POST", {
2552
+ body: request
2794
2553
  }),
2795
- adjustMastery: (request2) => client["request"]("/timeback/adjust-mastery", "POST", {
2796
- body: request2
2554
+ adjustMastery: (request) => client["request"]("/timeback/adjust-mastery", "POST", {
2555
+ body: request
2797
2556
  }),
2798
- toggleCompletion: (request2) => client["request"]("/timeback/toggle-completion", "POST", {
2799
- body: request2
2557
+ toggleCompletion: (request) => client["request"]("/timeback/toggle-completion", "POST", {
2558
+ body: request
2800
2559
  })
2801
2560
  }
2802
2561
  };
@@ -3105,6 +2864,143 @@ class ConnectionManager {
3105
2864
  }
3106
2865
  }
3107
2866
  }
2867
+ // src/core/transport/retry.ts
2868
+ var RETRY_DELAYS_MS = [500, 1500];
2869
+ function wait(ms) {
2870
+ return new Promise((resolve) => setTimeout(resolve, ms));
2871
+ }
2872
+ var retryRuntime = { wait };
2873
+ function isRetryableStatus(status) {
2874
+ return status === 429 || status >= 500;
2875
+ }
2876
+ async function fetchWithRetry(url, init2) {
2877
+ const startedAt = Date.now();
2878
+ for (let attempt = 0;attempt <= RETRY_DELAYS_MS.length; attempt++) {
2879
+ const retryDelayMs = RETRY_DELAYS_MS[attempt];
2880
+ const canRetry = init2.method === "GET" && retryDelayMs !== undefined;
2881
+ try {
2882
+ const response = await fetch(url, init2);
2883
+ if (canRetry && isRetryableStatus(response.status)) {
2884
+ await retryRuntime.wait(retryDelayMs);
2885
+ } else {
2886
+ return { response, retryCount: attempt, durationMs: Date.now() - startedAt };
2887
+ }
2888
+ } catch (error) {
2889
+ if (canRetry && error instanceof TypeError) {
2890
+ await retryRuntime.wait(retryDelayMs);
2891
+ } else {
2892
+ return { error, retryCount: attempt, durationMs: Date.now() - startedAt };
2893
+ }
2894
+ }
2895
+ }
2896
+ return {
2897
+ error: new PlaycademyError("Request failed after exhausting retries"),
2898
+ retryCount: RETRY_DELAYS_MS.length,
2899
+ durationMs: Date.now() - startedAt
2900
+ };
2901
+ }
2902
+ function unwrapFetchResult(result) {
2903
+ if (result.error) {
2904
+ throw result.error;
2905
+ }
2906
+ if (!result.response) {
2907
+ throw new PlaycademyError("Request failed after exhausting retries");
2908
+ }
2909
+ return result.response;
2910
+ }
2911
+ // src/core/transport/request.ts
2912
+ function prepareRequestBody(body, headers) {
2913
+ if (body instanceof FormData) {
2914
+ return body;
2915
+ }
2916
+ if (body instanceof ArrayBuffer || body instanceof Blob || ArrayBuffer.isView(body)) {
2917
+ if (!headers["Content-Type"]) {
2918
+ headers["Content-Type"] = "application/octet-stream";
2919
+ }
2920
+ return body;
2921
+ }
2922
+ if (body !== undefined && body !== null) {
2923
+ if (headers["Content-Type"]) {
2924
+ return typeof body === "string" ? body : JSON.stringify(body);
2925
+ }
2926
+ headers["Content-Type"] = "application/json";
2927
+ return JSON.stringify(body);
2928
+ }
2929
+ return;
2930
+ }
2931
+ function checkDevWarnings(data) {
2932
+ if (!data || typeof data !== "object") {
2933
+ return;
2934
+ }
2935
+ const response = data;
2936
+ const warningType = response.__playcademyDevWarning;
2937
+ if (!warningType) {
2938
+ return;
2939
+ }
2940
+ switch (warningType) {
2941
+ case "timeback-not-configured": {
2942
+ console.warn("%c⚠️ TimeBack Not Configured", "background: #f59e0b; color: white; padding: 6px 12px; border-radius: 4px; font-weight: bold; font-size: 13px");
2943
+ console.log("%cTimeBack is configured in playcademy.config.js but the sandbox does not have TimeBack credentials.", "color: #f59e0b; font-weight: 500");
2944
+ console.log("To test TimeBack locally:");
2945
+ console.log(" Set the following environment variables:");
2946
+ console.log(" • %cTIMEBACK_ONEROSTER_API_URL", "color: #0ea5e9; font-weight: 600; font-family: monospace");
2947
+ console.log(" • %cTIMEBACK_CALIPER_API_URL", "color: #0ea5e9; font-weight: 600; font-family: monospace");
2948
+ console.log(" • %cTIMEBACK_API_CLIENT_ID/SECRET", "color: #0ea5e9; font-weight: 600; font-family: monospace");
2949
+ console.log(" Or deploy your game: %cplaycademy deploy", "color: #10b981; font-weight: 600; font-family: monospace");
2950
+ console.log(" Or wait for %c@superbuilders/timeback-local%c (coming soon)", "color: #8b5cf6; font-weight: 600; font-family: monospace", "color: inherit");
2951
+ break;
2952
+ }
2953
+ default: {
2954
+ console.warn(`[Playcademy Dev Warning] ${warningType}`);
2955
+ }
2956
+ }
2957
+ }
2958
+ async function request({
2959
+ path,
2960
+ baseUrl,
2961
+ method = "GET",
2962
+ body,
2963
+ extraHeaders = {},
2964
+ raw = false
2965
+ }) {
2966
+ const url = baseUrl.replace(/\/$/, "") + (path.startsWith("/") ? path : `/${path}`);
2967
+ const headers = { ...extraHeaders };
2968
+ const payload = prepareRequestBody(body, headers);
2969
+ const res = unwrapFetchResult(await fetchWithRetry(url, {
2970
+ method,
2971
+ headers,
2972
+ body: payload,
2973
+ credentials: "omit"
2974
+ }));
2975
+ if (raw) {
2976
+ return res;
2977
+ }
2978
+ if (!res.ok) {
2979
+ const clonedRes = res.clone();
2980
+ const errorBody = await clonedRes.json().catch(() => clonedRes.text().catch(() => {
2981
+ return;
2982
+ })) ?? undefined;
2983
+ throw ApiError.fromResponse(res.status, res.statusText, errorBody);
2984
+ }
2985
+ if (res.status === 204) {
2986
+ return;
2987
+ }
2988
+ const contentType = res.headers.get("content-type") ?? "";
2989
+ if (contentType.includes("application/json")) {
2990
+ try {
2991
+ const parsed = await res.json();
2992
+ checkDevWarnings(parsed);
2993
+ return parsed;
2994
+ } catch (error) {
2995
+ if (error instanceof SyntaxError) {
2996
+ return;
2997
+ }
2998
+ throw error;
2999
+ }
3000
+ }
3001
+ const rawText = await res.text().catch(() => "");
3002
+ return rawText && rawText.length > 0 ? rawText : undefined;
3003
+ }
3108
3004
  // src/clients/base.ts
3109
3005
  class PlaycademyBaseClient {
3110
3006
  baseUrl;
@@ -3321,14 +3217,12 @@ class PlaycademyInternalClient extends PlaycademyBaseClient {
3321
3217
  static identity = identity;
3322
3218
  }
3323
3219
  export {
3324
- probeCorsReachability,
3325
3220
  messaging,
3326
3221
  extractApiErrorInfo,
3327
3222
  PlaycademyInternalClient,
3328
3223
  PlaycademyError,
3329
3224
  PlaycademyInternalClient as PlaycademyClient,
3330
3225
  MessageEvents,
3331
- ManifestError,
3332
3226
  ConnectionMonitor,
3333
3227
  ConnectionManager,
3334
3228
  ApiError
package/dist/server.js CHANGED
@@ -82,18 +82,6 @@ class PlaycademyError extends Error {
82
82
  }
83
83
  }
84
84
 
85
- class ManifestError extends PlaycademyError {
86
- kind;
87
- details;
88
- constructor(message, kind, details) {
89
- super(message);
90
- this.name = "ManifestError";
91
- this.kind = kind;
92
- this.details = details;
93
- Object.setPrototypeOf(this, ManifestError.prototype);
94
- }
95
- }
96
-
97
85
  class ApiError extends Error {
98
86
  code;
99
87
  details;
package/dist/types.d.ts CHANGED
@@ -22,36 +22,6 @@ declare function parseOAuthState(state: string): {
22
22
  /** Permitted HTTP verbs */
23
23
  type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
24
24
 
25
- /**
26
- * Game Types
27
- *
28
- * Literal types and API DTOs. Database row types are in @playcademy/data/types.
29
- *
30
- * @module types/game
31
- */
32
- type GameType = 'hosted' | 'external';
33
- type GamePlatform = 'web' | 'godot' | 'unity' | (string & {});
34
- /**
35
- * Game manifest file format (manifest.json).
36
- * Note: createdAt is a string here because it's parsed from JSON file.
37
- */
38
- interface ManifestV1 {
39
- version: string;
40
- platform: string;
41
- createdAt: string;
42
- }
43
- interface DomainValidationRecords {
44
- ownership?: {
45
- name?: string;
46
- value?: string;
47
- type?: string;
48
- };
49
- ssl?: {
50
- txt_name?: string;
51
- txt_value?: string;
52
- }[];
53
- }
54
-
55
25
  /**
56
26
  * User Types
57
27
  *
@@ -132,6 +102,36 @@ interface GameUser {
132
102
  timeback?: UserTimebackData;
133
103
  }
134
104
 
105
+ /**
106
+ * Game Types
107
+ *
108
+ * Literal types and API DTOs. Database row types are in @playcademy/data/types.
109
+ *
110
+ * @module types/game
111
+ */
112
+ type GameType = 'hosted' | 'external';
113
+ type GamePlatform = 'web' | 'godot' | 'unity' | (string & {});
114
+ /**
115
+ * Game manifest file format (manifest.json).
116
+ * Note: createdAt is a string here because it's parsed from JSON file.
117
+ */
118
+ interface ManifestV1 {
119
+ version: string;
120
+ platform: string;
121
+ createdAt: string;
122
+ }
123
+ interface DomainValidationRecords {
124
+ ownership?: {
125
+ name?: string;
126
+ value?: string;
127
+ type?: string;
128
+ };
129
+ ssl?: {
130
+ txt_name?: string;
131
+ txt_value?: string;
132
+ }[];
133
+ }
134
+
135
135
  /**
136
136
  * Leaderboard Types
137
137
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/sdk",
3
- "version": "0.3.6-beta.2",
3
+ "version": "0.3.6-beta.4",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {