@playcademy/sdk 0.0.7 → 0.0.9
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 +794 -72
- package/dist/index.js +566 -225
- package/dist/server.d.ts +309 -16
- package/dist/server.js +321 -5
- package/dist/types.d.ts +955 -250
- package/package.json +11 -8
package/dist/server.d.ts
CHANGED
|
@@ -1,3 +1,145 @@
|
|
|
1
|
+
import * as _playcademy_timeback_types from '@playcademy/timeback/types';
|
|
2
|
+
import { OrganizationConfig, CourseConfig, ComponentConfig, ResourceConfig, ComponentResourceConfig } from '@playcademy/timeback/types';
|
|
3
|
+
export { ComponentConfig, ComponentResourceConfig, CourseConfig, OrganizationConfig, ProgressData, ResourceConfig, SessionData, TimebackGrade, TimebackSubject, XPAwardMetadata } from '@playcademy/timeback/types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @fileoverview Server SDK Type Definitions
|
|
7
|
+
*
|
|
8
|
+
* TypeScript type definitions for the server-side Playcademy SDK.
|
|
9
|
+
* Includes configuration types, client state, and re-exported TimeBack types.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* TimeBack integration configuration for Playcademy config file
|
|
14
|
+
*/
|
|
15
|
+
interface TimebackIntegrationConfig {
|
|
16
|
+
/** Organization overrides */
|
|
17
|
+
organization?: Partial<OrganizationConfig>;
|
|
18
|
+
/** Course configuration (subjects and grades REQUIRED) */
|
|
19
|
+
course: CourseConfig;
|
|
20
|
+
/** Component overrides */
|
|
21
|
+
component?: Partial<ComponentConfig>;
|
|
22
|
+
/** Resource overrides */
|
|
23
|
+
resource?: Partial<ResourceConfig>;
|
|
24
|
+
/** Component-Resource link overrides */
|
|
25
|
+
componentResource?: Partial<ComponentResourceConfig>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Integrations configuration
|
|
29
|
+
*/
|
|
30
|
+
interface IntegrationsConfig {
|
|
31
|
+
/** TimeBack integration (optional) */
|
|
32
|
+
timeback?: TimebackIntegrationConfig;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Unified Playcademy configuration
|
|
36
|
+
* Used for playcademy.config.{js,json}
|
|
37
|
+
*/
|
|
38
|
+
interface PlaycademyConfig {
|
|
39
|
+
/** Game name */
|
|
40
|
+
name: string;
|
|
41
|
+
/** Game description */
|
|
42
|
+
description?: string;
|
|
43
|
+
/** Game emoji icon */
|
|
44
|
+
emoji?: string;
|
|
45
|
+
/** Build command to run before deployment */
|
|
46
|
+
buildCommand?: string[];
|
|
47
|
+
/** Path to build output */
|
|
48
|
+
buildPath?: string;
|
|
49
|
+
/** Game type */
|
|
50
|
+
gameType?: 'hosted' | 'external';
|
|
51
|
+
/** External URL (for external games) */
|
|
52
|
+
externalUrl?: string;
|
|
53
|
+
/** Game platform */
|
|
54
|
+
platform?: 'web' | 'unity' | 'godot';
|
|
55
|
+
/** Backend configuration */
|
|
56
|
+
backend?: {
|
|
57
|
+
/** Custom API routes directory (defaults to 'api') */
|
|
58
|
+
directory?: string;
|
|
59
|
+
};
|
|
60
|
+
/** External integrations */
|
|
61
|
+
integrations?: IntegrationsConfig;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Configuration options for initializing a PlaycademyClient instance.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* const config: PlaycademyServerClientConfig = {
|
|
70
|
+
* apiKey: process.env.PLAYCADEMY_API_KEY!,
|
|
71
|
+
* gameId: 'my-math-game',
|
|
72
|
+
* configPath: './playcademy.config.js'
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
interface PlaycademyServerClientConfig {
|
|
77
|
+
/**
|
|
78
|
+
* Playcademy API key for server-to-server authentication.
|
|
79
|
+
* Obtain from the Playcademy developer dashboard.
|
|
80
|
+
*/
|
|
81
|
+
apiKey: string;
|
|
82
|
+
/**
|
|
83
|
+
* Optional path to playcademy.config.js file.
|
|
84
|
+
* If not provided, searches current directory and up to 3 parent directories.
|
|
85
|
+
* Ignored if `config` is provided directly.
|
|
86
|
+
*
|
|
87
|
+
* @example './config/playcademy.config.js'
|
|
88
|
+
*/
|
|
89
|
+
configPath?: string;
|
|
90
|
+
/**
|
|
91
|
+
* Optional config object (for edge environments without filesystem).
|
|
92
|
+
* If provided, skips filesystem-based config loading.
|
|
93
|
+
*
|
|
94
|
+
* @example { name: 'My Game', integrations: { timeback: {...} } }
|
|
95
|
+
*/
|
|
96
|
+
config?: PlaycademyConfig;
|
|
97
|
+
/**
|
|
98
|
+
* Optional base URL for Playcademy API.
|
|
99
|
+
* Defaults to environment variables or 'https://hub.playcademy.com'.
|
|
100
|
+
*
|
|
101
|
+
* @example 'http://localhost:3000' for local development
|
|
102
|
+
*/
|
|
103
|
+
baseUrl?: string;
|
|
104
|
+
/**
|
|
105
|
+
* Optional game ID.
|
|
106
|
+
* If not provided, will attempt to fetch from API using the API token.
|
|
107
|
+
*
|
|
108
|
+
* @example 'my-math-game'
|
|
109
|
+
*/
|
|
110
|
+
gameId?: string;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Internal state maintained by the PlaycademyClient instance.
|
|
114
|
+
*
|
|
115
|
+
* @internal
|
|
116
|
+
*/
|
|
117
|
+
interface PlaycademyServerClientState {
|
|
118
|
+
/** API key for authentication */
|
|
119
|
+
apiKey: string;
|
|
120
|
+
/** Base URL for API requests */
|
|
121
|
+
baseUrl: string;
|
|
122
|
+
/** Game identifier */
|
|
123
|
+
gameId: string;
|
|
124
|
+
/** Loaded game configuration from playcademy.config.js */
|
|
125
|
+
config: PlaycademyConfig;
|
|
126
|
+
/**
|
|
127
|
+
* TimeBack course ID fetched from the Playcademy API.
|
|
128
|
+
* Used for all TimeBack event recording.
|
|
129
|
+
*/
|
|
130
|
+
courseId?: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Backend deployment bundle for uploading to Playcademy platform
|
|
135
|
+
*/
|
|
136
|
+
interface BackendDeploymentBundle {
|
|
137
|
+
/** Bundled JavaScript code ready for deployment */
|
|
138
|
+
code: string;
|
|
139
|
+
/** Game configuration */
|
|
140
|
+
config: PlaycademyConfig;
|
|
141
|
+
}
|
|
142
|
+
|
|
1
143
|
/**
|
|
2
144
|
* Basic user information in the shape of the claims from identity providers
|
|
3
145
|
*/
|
|
@@ -14,28 +156,183 @@ interface UserInfo {
|
|
|
14
156
|
given_name?: string;
|
|
15
157
|
/** Optional family name (last name) */
|
|
16
158
|
family_name?: string;
|
|
159
|
+
/** TimeBack student ID (if user has TimeBack integration) */
|
|
160
|
+
timeback_id?: string;
|
|
17
161
|
/** Additional user attributes from the identity provider */
|
|
18
162
|
[key: string]: unknown;
|
|
19
163
|
}
|
|
164
|
+
type RecordProgressResponse = {
|
|
165
|
+
status: 'ok';
|
|
166
|
+
courseId: string;
|
|
167
|
+
};
|
|
168
|
+
type RecordSessionEndResponse = {
|
|
169
|
+
status: 'ok';
|
|
170
|
+
courseId: string;
|
|
171
|
+
};
|
|
172
|
+
type AwardXpResponse = {
|
|
173
|
+
status: 'ok';
|
|
174
|
+
courseId: string;
|
|
175
|
+
xpAwarded: number;
|
|
176
|
+
};
|
|
20
177
|
|
|
21
178
|
/**
|
|
22
|
-
* Server-
|
|
179
|
+
* Server-side Playcademy client for recording student progress to TimeBack.
|
|
180
|
+
*
|
|
181
|
+
* This client automatically loads game configuration from playcademy.config.js
|
|
182
|
+
* and uses it to auto-fill TimeBack metadata (subject, appName, courseId).
|
|
23
183
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```typescript
|
|
186
|
+
* // Initialize the client (loads config automatically)
|
|
187
|
+
* const client = await PlaycademyClient.init({
|
|
188
|
+
* apiKey: process.env.PLAYCADEMY_API_KEY
|
|
189
|
+
* })
|
|
190
|
+
*
|
|
191
|
+
* // Record progress (metadata auto-filled from config)
|
|
192
|
+
* await client.timeback.recordProgress(studentId, {
|
|
193
|
+
* score: 85,
|
|
194
|
+
* totalQuestions: 20,
|
|
195
|
+
* correctQuestions: 17
|
|
196
|
+
* })
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
199
|
+
declare class PlaycademyClient {
|
|
200
|
+
private state;
|
|
201
|
+
/**
|
|
202
|
+
* Private constructor. Use PlaycademyClient.init() to create instances.
|
|
203
|
+
*
|
|
204
|
+
* @param state - Internal client state
|
|
205
|
+
* @private
|
|
206
|
+
*/
|
|
207
|
+
private constructor();
|
|
208
|
+
/**
|
|
209
|
+
* Initialize a new PlaycademyClient instance.
|
|
210
|
+
*
|
|
211
|
+
* Loads playcademy.config.js from the current directory (or specified path),
|
|
212
|
+
* validates the configuration, and fetches the TimeBack courseId from the API.
|
|
213
|
+
*
|
|
214
|
+
* @param config - Client configuration options
|
|
215
|
+
* @param config.apiKey - Playcademy API key for authentication
|
|
216
|
+
* @param config.gameId - Optional game ID (will be fetched from API if not provided)
|
|
217
|
+
* @param config.configPath - Optional path to playcademy.config.js (auto-discovered if not provided)
|
|
218
|
+
* @param config.baseUrl - Optional base URL for Playcademy API (defaults to production)
|
|
219
|
+
* @returns Promise resolving to initialized PlaycademyClient instance
|
|
220
|
+
* @throws {Error} If apiKey is missing or invalid
|
|
221
|
+
* @throws {Error} If config file cannot be found or loaded
|
|
222
|
+
* @throws {Error} If TimeBack integration is not set up for the game
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* ```typescript
|
|
226
|
+
* // Basic initialization with API key
|
|
227
|
+
* const client = await PlaycademyClient.init({
|
|
228
|
+
* apiKey: process.env.PLAYCADEMY_API_KEY,
|
|
229
|
+
* gameId: 'my-math-game'
|
|
230
|
+
* })
|
|
231
|
+
*
|
|
232
|
+
* // With custom config path
|
|
233
|
+
* const client = await PlaycademyClient.init({
|
|
234
|
+
* apiKey: process.env.PLAYCADEMY_API_KEY,
|
|
235
|
+
* configPath: './custom-config.js'
|
|
236
|
+
* })
|
|
237
|
+
*
|
|
238
|
+
* // With custom base URL (for development)
|
|
239
|
+
* const client = await PlaycademyClient.init({
|
|
240
|
+
* apiKey: process.env.PLAYCADEMY_API_KEY,
|
|
241
|
+
* baseUrl: 'http://localhost:3000'
|
|
242
|
+
* })
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
245
|
+
static init(config: PlaycademyServerClientConfig): Promise<PlaycademyClient>;
|
|
246
|
+
/**
|
|
247
|
+
* Fetch gameId from API using the API token.
|
|
248
|
+
*
|
|
249
|
+
* @private
|
|
250
|
+
* @throws {Error} Always throws - gameId fetching not yet implemented
|
|
251
|
+
* @todo Implement API endpoint to fetch gameId from API token
|
|
252
|
+
*/
|
|
253
|
+
private fetchGameId;
|
|
254
|
+
/**
|
|
255
|
+
* Makes an authenticated HTTP request to the API.
|
|
256
|
+
*
|
|
257
|
+
* @param path - API endpoint path
|
|
258
|
+
* @param method - HTTP method
|
|
259
|
+
* @param body - Request body (optional)
|
|
260
|
+
* @returns Promise resolving to the response data
|
|
261
|
+
* @protected
|
|
262
|
+
*/
|
|
263
|
+
protected request<T>(path: string, method?: 'GET' | 'POST' | 'PUT' | 'DELETE', body?: unknown): Promise<T>;
|
|
264
|
+
/**
|
|
265
|
+
* Gets the current game ID.
|
|
266
|
+
*
|
|
267
|
+
* @returns The game ID
|
|
268
|
+
*/
|
|
269
|
+
get gameId(): string;
|
|
270
|
+
/**
|
|
271
|
+
* Gets the loaded game configuration.
|
|
272
|
+
*
|
|
273
|
+
* Returns the configuration loaded from playcademy.config.js during
|
|
274
|
+
* client initialization.
|
|
275
|
+
*
|
|
276
|
+
* @returns The loaded configuration object
|
|
277
|
+
*/
|
|
278
|
+
get config(): PlaycademyServerClientState['config'];
|
|
279
|
+
/** TimeBack integration methods (recordProgress, recordSessionEnd, awardXP) */
|
|
280
|
+
timeback: {
|
|
281
|
+
readonly courseId: string | undefined;
|
|
282
|
+
recordProgress: (studentId: string, progressData: _playcademy_timeback_types.ProgressData) => Promise<RecordProgressResponse>;
|
|
283
|
+
recordSessionEnd: (studentId: string, sessionData: _playcademy_timeback_types.SessionData) => Promise<RecordSessionEndResponse>;
|
|
284
|
+
awardXP: (studentId: string, xpAmount: number, metadata: _playcademy_timeback_types.XPAwardMetadata) => Promise<AwardXpResponse>;
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* @fileoverview Game Token Verification Utilities
|
|
290
|
+
*
|
|
291
|
+
* Utilities for verifying Playcademy game tokens sent from external games.
|
|
27
292
|
*/
|
|
28
293
|
|
|
29
294
|
/**
|
|
30
295
|
* Verifies a short-lived Playcademy Game Token and returns verified identity claims.
|
|
31
296
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
297
|
+
* Game tokens are JWT tokens minted by Playcademy and sent to external games
|
|
298
|
+
* running in iframes. They contain verified user identity information and can
|
|
299
|
+
* be validated server-side to securely identify players.
|
|
300
|
+
*
|
|
301
|
+
* This function calls the Playcademy API's `/api/games/verify` endpoint to
|
|
302
|
+
* cryptographically verify the token and extract the user information.
|
|
303
|
+
*
|
|
304
|
+
* @param gameToken - The game JWT token string to verify
|
|
305
|
+
* @param options - Optional configuration
|
|
306
|
+
* @param options.baseUrl - Optional base URL override (defaults to env vars or production)
|
|
307
|
+
* @returns Promise resolving to verified token payload
|
|
308
|
+
* @returns claims - Arbitrary claims from the JWT payload
|
|
309
|
+
* @returns gameId - The game ID this token was issued for
|
|
310
|
+
* @returns user - Verified user information (id, email, name, etc.)
|
|
311
|
+
* @throws {Error} If gameToken is not a valid string
|
|
312
|
+
* @throws {Error} If PLAYCADEMY_BASE_URL is not set and no baseUrl provided
|
|
313
|
+
* @throws {Error} If token verification fails (invalid/expired token)
|
|
314
|
+
* @throws {Error} If network request fails
|
|
34
315
|
*
|
|
35
|
-
* @
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
316
|
+
* @example
|
|
317
|
+
* ```typescript
|
|
318
|
+
* // In your game server
|
|
319
|
+
* import { verifyGameToken } from '@playcademy/sdk/server'
|
|
320
|
+
*
|
|
321
|
+
* app.post('/api/auth/playcademy', async (req, res) => {
|
|
322
|
+
* const { gameToken } = req.body
|
|
323
|
+
*
|
|
324
|
+
* try {
|
|
325
|
+
* const { user, gameId, claims } = await verifyGameToken(gameToken)
|
|
326
|
+
*
|
|
327
|
+
* // Create session for the verified user
|
|
328
|
+
* const session = await createSession(user.id)
|
|
329
|
+
*
|
|
330
|
+
* res.json({ success: true, user, session })
|
|
331
|
+
* } catch (error) {
|
|
332
|
+
* res.status(401).json({ error: 'Invalid token' })
|
|
333
|
+
* }
|
|
334
|
+
* })
|
|
335
|
+
* ```
|
|
39
336
|
*/
|
|
40
337
|
declare function verifyGameToken(gameToken: string, options?: {
|
|
41
338
|
baseUrl?: string;
|
|
@@ -44,10 +341,6 @@ declare function verifyGameToken(gameToken: string, options?: {
|
|
|
44
341
|
gameId: string;
|
|
45
342
|
user: UserInfo;
|
|
46
343
|
}>;
|
|
47
|
-
declare const PlaycademyServer: {
|
|
48
|
-
auth: {
|
|
49
|
-
verifyGameToken: typeof verifyGameToken;
|
|
50
|
-
};
|
|
51
|
-
};
|
|
52
344
|
|
|
53
|
-
export {
|
|
345
|
+
export { PlaycademyClient, verifyGameToken };
|
|
346
|
+
export type { BackendDeploymentBundle, IntegrationsConfig, PlaycademyConfig, PlaycademyServerClientConfig, PlaycademyServerClientState, TimebackIntegrationConfig };
|
package/dist/server.js
CHANGED
|
@@ -10,7 +10,326 @@ var __export = (target, all) => {
|
|
|
10
10
|
};
|
|
11
11
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
12
12
|
|
|
13
|
-
// src/server.ts
|
|
13
|
+
// src/server/namespaces/timeback.ts
|
|
14
|
+
function createTimebackNamespace(client) {
|
|
15
|
+
let courseId;
|
|
16
|
+
async function ensureCourseId() {
|
|
17
|
+
if (courseId)
|
|
18
|
+
return courseId;
|
|
19
|
+
try {
|
|
20
|
+
const integration = await client["request"](`/api/timeback/integrations/${client.gameId}`, "GET");
|
|
21
|
+
if (!integration || !integration.courseId) {
|
|
22
|
+
throw new Error("No TimeBack integration found for this game. Please run TimeBack setup first.");
|
|
23
|
+
}
|
|
24
|
+
courseId = integration.courseId;
|
|
25
|
+
return courseId;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw new Error(`Failed to fetch courseId: ${error instanceof Error ? error.message : String(error)}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function enrichProgressData(data) {
|
|
31
|
+
return {
|
|
32
|
+
...data,
|
|
33
|
+
subject: data.subject || client.config.integrations?.timeback?.course.subjects?.[0],
|
|
34
|
+
appName: data.appName || client.config.name,
|
|
35
|
+
activityName: data.activityName || client.config.name,
|
|
36
|
+
courseName: data.courseName || client.config.integrations?.timeback?.course.title
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function enrichSessionData(data) {
|
|
40
|
+
return {
|
|
41
|
+
...data,
|
|
42
|
+
subject: data.subject || client.config.integrations?.timeback?.course.subjects?.[0],
|
|
43
|
+
appName: data.appName || client.config.name,
|
|
44
|
+
activityName: data.activityName || client.config.name,
|
|
45
|
+
courseName: data.courseName || client.config.integrations?.timeback?.course.title
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function enrichXPMetadata(data) {
|
|
49
|
+
return {
|
|
50
|
+
...data,
|
|
51
|
+
subject: data.subject || client.config.integrations?.timeback?.course.subjects?.[0],
|
|
52
|
+
appName: data.appName || client.config.name,
|
|
53
|
+
activityName: data.activityName || client.config.name,
|
|
54
|
+
courseName: data.courseName || client.config.integrations?.timeback?.course.title
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
get courseId() {
|
|
59
|
+
return courseId;
|
|
60
|
+
},
|
|
61
|
+
recordProgress: async (studentId, progressData) => {
|
|
62
|
+
await ensureCourseId();
|
|
63
|
+
const enrichedData = enrichProgressData(progressData);
|
|
64
|
+
return client["request"]("/api/timeback/progress", "POST", {
|
|
65
|
+
gameId: client.gameId,
|
|
66
|
+
studentId,
|
|
67
|
+
progressData: enrichedData
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
recordSessionEnd: async (studentId, sessionData) => {
|
|
71
|
+
await ensureCourseId();
|
|
72
|
+
const enrichedData = enrichSessionData(sessionData);
|
|
73
|
+
return client["request"]("/api/timeback/session-end", "POST", {
|
|
74
|
+
gameId: client.gameId,
|
|
75
|
+
studentId,
|
|
76
|
+
sessionData: enrichedData
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
awardXP: async (studentId, xpAmount, metadata) => {
|
|
80
|
+
await ensureCourseId();
|
|
81
|
+
if (typeof xpAmount !== "number" || xpAmount <= 0) {
|
|
82
|
+
throw new Error("[Playcademy SDK] xpAmount must be a positive number");
|
|
83
|
+
}
|
|
84
|
+
const enrichedMetadata = enrichXPMetadata(metadata);
|
|
85
|
+
return client["request"]("/api/timeback/award-xp", "POST", {
|
|
86
|
+
gameId: client.gameId,
|
|
87
|
+
studentId,
|
|
88
|
+
xpAmount,
|
|
89
|
+
metadata: enrichedMetadata
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// src/server/request.ts
|
|
95
|
+
async function makeApiRequest(baseUrl, apiToken, endpoint, method = "GET", body) {
|
|
96
|
+
const url = `${baseUrl}${endpoint}`;
|
|
97
|
+
const headers = new Headers;
|
|
98
|
+
headers.set("Content-Type", "application/json");
|
|
99
|
+
headers.set("x-api-key", apiToken);
|
|
100
|
+
const options = {
|
|
101
|
+
method,
|
|
102
|
+
headers
|
|
103
|
+
};
|
|
104
|
+
if (body && (method === "POST" || method === "PUT")) {
|
|
105
|
+
options.body = JSON.stringify(body);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const response = await fetch(url, options);
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
const errorText = await response.text().catch(() => "Unknown error");
|
|
111
|
+
throw new Error(`API request failed: ${response.status} ${errorText}`);
|
|
112
|
+
}
|
|
113
|
+
return await response.json();
|
|
114
|
+
} catch (error) {
|
|
115
|
+
throw new Error(`Request failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/server/utils/config-loader.ts
|
|
120
|
+
import { resolve as resolve2 } from "path";
|
|
121
|
+
|
|
122
|
+
// ../utils/src/file-loader.ts
|
|
123
|
+
import { existsSync, readdirSync, statSync } from "fs";
|
|
124
|
+
import { readFile } from "fs/promises";
|
|
125
|
+
import { dirname, parse, resolve } from "path";
|
|
126
|
+
function findFilePath(filename, startDir, maxLevels = 3) {
|
|
127
|
+
const filenames = Array.isArray(filename) ? filename : [filename];
|
|
128
|
+
let currentDir = resolve(startDir);
|
|
129
|
+
let levelsSearched = 0;
|
|
130
|
+
while (levelsSearched <= maxLevels) {
|
|
131
|
+
for (const fname of filenames) {
|
|
132
|
+
const filePath = resolve(currentDir, fname);
|
|
133
|
+
if (existsSync(filePath)) {
|
|
134
|
+
return {
|
|
135
|
+
path: filePath,
|
|
136
|
+
dir: currentDir,
|
|
137
|
+
filename: fname
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (levelsSearched >= maxLevels) {
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
const parentDir = dirname(currentDir);
|
|
145
|
+
if (parentDir === currentDir) {
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
const parsed = parse(currentDir);
|
|
149
|
+
if (parsed.root === currentDir) {
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
currentDir = parentDir;
|
|
153
|
+
levelsSearched++;
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
async function loadFile(filename, options = {}) {
|
|
158
|
+
const {
|
|
159
|
+
cwd = process.cwd(),
|
|
160
|
+
required = false,
|
|
161
|
+
searchUp = false,
|
|
162
|
+
maxLevels = 3,
|
|
163
|
+
parseJson = false
|
|
164
|
+
} = options;
|
|
165
|
+
let fileResult;
|
|
166
|
+
if (searchUp) {
|
|
167
|
+
fileResult = findFilePath(filename, cwd, maxLevels);
|
|
168
|
+
} else {
|
|
169
|
+
const filenames = Array.isArray(filename) ? filename : [filename];
|
|
170
|
+
fileResult = null;
|
|
171
|
+
for (const fname of filenames) {
|
|
172
|
+
const filePath = resolve(cwd, fname);
|
|
173
|
+
if (existsSync(filePath)) {
|
|
174
|
+
fileResult = {
|
|
175
|
+
path: filePath,
|
|
176
|
+
dir: cwd,
|
|
177
|
+
filename: fname
|
|
178
|
+
};
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (!fileResult) {
|
|
184
|
+
if (required) {
|
|
185
|
+
const fileList = Array.isArray(filename) ? filename.join(" or ") : filename;
|
|
186
|
+
const message = searchUp ? `${fileList} not found in ${cwd} or up to ${maxLevels} parent directories` : `${fileList} not found at ${cwd}`;
|
|
187
|
+
throw new Error(message);
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
const content = await readFile(fileResult.path, "utf-8");
|
|
193
|
+
if (parseJson) {
|
|
194
|
+
return JSON.parse(content);
|
|
195
|
+
}
|
|
196
|
+
return content;
|
|
197
|
+
} catch (error) {
|
|
198
|
+
throw new Error(`Failed to load ${fileResult.filename} from ${fileResult.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function findFile(filename, options = {}) {
|
|
202
|
+
const { cwd = process.cwd(), searchUp = false, maxLevels = 3 } = options;
|
|
203
|
+
if (searchUp) {
|
|
204
|
+
return findFilePath(filename, cwd, maxLevels);
|
|
205
|
+
}
|
|
206
|
+
const filenames = Array.isArray(filename) ? filename : [filename];
|
|
207
|
+
for (const fname of filenames) {
|
|
208
|
+
const filePath = resolve(cwd, fname);
|
|
209
|
+
if (existsSync(filePath)) {
|
|
210
|
+
return {
|
|
211
|
+
path: filePath,
|
|
212
|
+
dir: cwd,
|
|
213
|
+
filename: fname
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/server/utils/config-loader.ts
|
|
221
|
+
async function findConfigPath(configPath) {
|
|
222
|
+
if (configPath) {
|
|
223
|
+
return resolve2(configPath);
|
|
224
|
+
}
|
|
225
|
+
const result = await findFile(["playcademy.config.js", "playcademy.config.mjs", "playcademy.config.json"], {
|
|
226
|
+
searchUp: true,
|
|
227
|
+
maxLevels: 3
|
|
228
|
+
});
|
|
229
|
+
if (!result) {
|
|
230
|
+
throw new Error("playcademy.config.js not found. Please create a playcademy.config.js file or specify the config path.");
|
|
231
|
+
}
|
|
232
|
+
return result.path;
|
|
233
|
+
}
|
|
234
|
+
async function loadConfig(configPath) {
|
|
235
|
+
try {
|
|
236
|
+
let config;
|
|
237
|
+
let actualPath;
|
|
238
|
+
if (configPath) {
|
|
239
|
+
actualPath = resolve2(configPath);
|
|
240
|
+
if (actualPath.endsWith(".json")) {
|
|
241
|
+
config = await loadFile(actualPath, { required: true, parseJson: true });
|
|
242
|
+
} else {
|
|
243
|
+
const module = await import(actualPath);
|
|
244
|
+
config = module.default || module;
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
actualPath = await findConfigPath();
|
|
248
|
+
if (actualPath.endsWith(".json")) {
|
|
249
|
+
config = await loadFile(actualPath, { required: true, parseJson: true });
|
|
250
|
+
} else {
|
|
251
|
+
const module = await import(actualPath);
|
|
252
|
+
config = module.default || module;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (!config || typeof config !== "object") {
|
|
256
|
+
throw new Error("Config file must export/contain an object");
|
|
257
|
+
}
|
|
258
|
+
validateConfig(config);
|
|
259
|
+
return config;
|
|
260
|
+
} catch (error) {
|
|
261
|
+
throw new Error(`Failed to load config file: ${error instanceof Error ? error.message : String(error)}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function validateConfig(config) {
|
|
265
|
+
if (!config || typeof config !== "object") {
|
|
266
|
+
throw new Error("Configuration must be an object");
|
|
267
|
+
}
|
|
268
|
+
const cfg = config;
|
|
269
|
+
if (!cfg.name || typeof cfg.name !== "string") {
|
|
270
|
+
throw new Error("Missing required field: name");
|
|
271
|
+
}
|
|
272
|
+
if (cfg.timeback) {
|
|
273
|
+
if (typeof cfg.timeback !== "object") {
|
|
274
|
+
throw new Error("timeback must be an object if provided");
|
|
275
|
+
}
|
|
276
|
+
const tb = cfg.timeback;
|
|
277
|
+
if (!tb.course) {
|
|
278
|
+
throw new Error("timeback.course is required for TimeBack integration");
|
|
279
|
+
}
|
|
280
|
+
if (typeof tb.course !== "object") {
|
|
281
|
+
throw new Error("timeback.course must be an object");
|
|
282
|
+
}
|
|
283
|
+
const course = tb.course;
|
|
284
|
+
if (!course.subjects || !Array.isArray(course.subjects) || course.subjects.length === 0) {
|
|
285
|
+
throw new Error("timeback.course.subjects is required (array of subjects)");
|
|
286
|
+
}
|
|
287
|
+
if (!course.grades || !Array.isArray(course.grades) || course.grades.length === 0) {
|
|
288
|
+
throw new Error("timeback.course.grades is required (array of grade levels)");
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/server/client.ts
|
|
294
|
+
class PlaycademyClient {
|
|
295
|
+
state;
|
|
296
|
+
constructor(state) {
|
|
297
|
+
this.state = state;
|
|
298
|
+
}
|
|
299
|
+
static async init(config) {
|
|
300
|
+
const { apiKey, configPath, baseUrl, gameId } = config;
|
|
301
|
+
if (!apiKey || typeof apiKey !== "string") {
|
|
302
|
+
throw new Error("[Playcademy SDK] apiKey is required");
|
|
303
|
+
}
|
|
304
|
+
const finalBaseUrl = baseUrl || process.env.PLAYCADEMY_BASE_URL || "https://hub.playcademy.com";
|
|
305
|
+
const loadedConfig = config.config || await loadConfig(configPath);
|
|
306
|
+
const state = {
|
|
307
|
+
apiKey,
|
|
308
|
+
baseUrl: finalBaseUrl,
|
|
309
|
+
gameId: gameId || "",
|
|
310
|
+
config: loadedConfig
|
|
311
|
+
};
|
|
312
|
+
const client = new PlaycademyClient(state);
|
|
313
|
+
if (!gameId) {
|
|
314
|
+
await client.fetchGameId();
|
|
315
|
+
}
|
|
316
|
+
return client;
|
|
317
|
+
}
|
|
318
|
+
async fetchGameId() {
|
|
319
|
+
throw new Error("[Playcademy SDK] gameId is required. Please provide it in the config or implement gameId fetching.");
|
|
320
|
+
}
|
|
321
|
+
async request(path, method = "GET", body) {
|
|
322
|
+
return makeApiRequest(this.state.baseUrl, this.state.apiKey, path, method, body);
|
|
323
|
+
}
|
|
324
|
+
get gameId() {
|
|
325
|
+
return this.state.gameId;
|
|
326
|
+
}
|
|
327
|
+
get config() {
|
|
328
|
+
return this.state.config;
|
|
329
|
+
}
|
|
330
|
+
timeback = createTimebackNamespace(this);
|
|
331
|
+
}
|
|
332
|
+
// src/server/utils/verify-game-token.ts
|
|
14
333
|
async function verifyGameToken(gameToken, options) {
|
|
15
334
|
if (!gameToken || typeof gameToken !== "string") {
|
|
16
335
|
throw new Error("[Playcademy SDK] gameToken must be a non-empty string");
|
|
@@ -41,10 +360,7 @@ Please set the PLAYCADEMY_BASE_URL environment variable`);
|
|
|
41
360
|
throw new Error("[Playcademy SDK] Token verification failed: Network error");
|
|
42
361
|
}
|
|
43
362
|
}
|
|
44
|
-
var PlaycademyServer = {
|
|
45
|
-
auth: { verifyGameToken }
|
|
46
|
-
};
|
|
47
363
|
export {
|
|
48
364
|
verifyGameToken,
|
|
49
|
-
|
|
365
|
+
PlaycademyClient
|
|
50
366
|
};
|