@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/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-only utilities for Playcademy.
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
- * NOTE: This module is intended for backend/server runtimes. It should not be
25
- * bundled into browser code. The API intentionally avoids requiring a
26
- * PlaycademyClient instance, offering stateless helpers for auth flows.
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
- * This calls the Playcademy API to cryptographically verify the token and
33
- * returns the verified user information and claims.
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
- * @param gameToken - The game JWT token to verify
36
- * @param options - Optional configuration (reserved for future use)
37
- * @returns Promise containing verified claims, gameId, and user information
38
- * @throws Error if token is invalid or verification fails
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 { PlaycademyServer, verifyGameToken };
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
- PlaycademyServer
365
+ PlaycademyClient
50
366
  };