@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 +1 -16
- package/dist/index.js +0 -104
- package/dist/internal.d.ts +228 -256
- package/dist/internal.js +148 -254
- package/dist/server.js +0 -12
- package/dist/types.d.ts +30 -30
- package/package.json +1 -1
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
|
|
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) {
|
package/dist/internal.d.ts
CHANGED
|
@@ -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' | '
|
|
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,
|
|
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,
|
|
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
|
|
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: (
|
|
2713
|
-
body:
|
|
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: (
|
|
2790
|
-
body:
|
|
2548
|
+
grantXp: (request) => client["request"]("/timeback/grant-xp", "POST", {
|
|
2549
|
+
body: request
|
|
2791
2550
|
}),
|
|
2792
|
-
adjustTime: (
|
|
2793
|
-
body:
|
|
2551
|
+
adjustTime: (request) => client["request"]("/timeback/adjust-time", "POST", {
|
|
2552
|
+
body: request
|
|
2794
2553
|
}),
|
|
2795
|
-
adjustMastery: (
|
|
2796
|
-
body:
|
|
2554
|
+
adjustMastery: (request) => client["request"]("/timeback/adjust-mastery", "POST", {
|
|
2555
|
+
body: request
|
|
2797
2556
|
}),
|
|
2798
|
-
toggleCompletion: (
|
|
2799
|
-
body:
|
|
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
|
*
|