@playcademy/sdk 0.3.7-beta.1 → 0.3.7-beta.2

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.
@@ -0,0 +1,323 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, {
5
+ get: all[name],
6
+ enumerable: true,
7
+ configurable: true,
8
+ set: (newValue) => all[name] = () => newValue
9
+ });
10
+ };
11
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
12
+
13
+ // src/core/guards.ts
14
+ var VALID_GRADES = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
15
+ var VALID_SUBJECTS = [
16
+ "Reading",
17
+ "Language",
18
+ "Vocabulary",
19
+ "Social Studies",
20
+ "Writing",
21
+ "Science",
22
+ "FastMath",
23
+ "Math",
24
+ "None"
25
+ ];
26
+ function isValidGrade(value) {
27
+ return typeof value === "number" && Number.isInteger(value) && VALID_GRADES.includes(value);
28
+ }
29
+ function isValidSubject(value) {
30
+ return typeof value === "string" && VALID_SUBJECTS.includes(value);
31
+ }
32
+ // src/server/namespaces/timeback.ts
33
+ function createTimebackNamespace(client) {
34
+ function enrichActivityData(data) {
35
+ return {
36
+ ...data,
37
+ appName: data.appName || client.config.name
38
+ };
39
+ }
40
+ return {
41
+ endActivity: async (studentId, payload) => {
42
+ if (!isValidGrade(payload.activityData.grade)) {
43
+ throw new Error("activityData.grade must be a valid grade level (-1 to 13)");
44
+ }
45
+ if (!isValidSubject(payload.activityData.subject)) {
46
+ throw new Error("activityData.subject must be a valid subject");
47
+ }
48
+ const enrichedActivityData = enrichActivityData(payload.activityData);
49
+ return client["request"]("/api/timeback/end-activity", "POST", {
50
+ gameId: client.gameId,
51
+ studentId,
52
+ activityData: enrichedActivityData,
53
+ scoreData: payload.scoreData,
54
+ timingData: payload.timingData,
55
+ xpEarned: payload.xpEarned,
56
+ masteredUnits: payload.masteredUnits,
57
+ extensions: payload.extensions
58
+ });
59
+ },
60
+ getStudentXp: async (studentId, options) => {
61
+ const hasGrade = options?.grade !== undefined;
62
+ const hasSubject = options?.subject !== undefined;
63
+ if (hasGrade !== hasSubject) {
64
+ throw new Error("Both grade and subject must be provided together");
65
+ }
66
+ if (hasGrade && !isValidGrade(options.grade)) {
67
+ throw new Error(`Invalid grade: ${options.grade}. Valid grades: ${VALID_GRADES.join(", ")}`);
68
+ }
69
+ if (hasSubject && !isValidSubject(options.subject)) {
70
+ throw new Error(`Invalid subject: ${options.subject}. Valid subjects: ${VALID_SUBJECTS.join(", ")}`);
71
+ }
72
+ const params = new URLSearchParams;
73
+ params.set("gameId", client.gameId);
74
+ if (options?.grade !== undefined) {
75
+ params.set("grade", String(options.grade));
76
+ }
77
+ if (options?.subject) {
78
+ params.set("subject", options.subject);
79
+ }
80
+ if (options?.include?.length) {
81
+ params.set("include", options.include.join(","));
82
+ }
83
+ const queryString = params.toString();
84
+ const endpoint = `/api/timeback/student-xp/${studentId}?${queryString}`;
85
+ return client["request"](endpoint, "GET");
86
+ }
87
+ };
88
+ }
89
+ // src/core/errors.ts
90
+ class PlaycademyError extends Error {
91
+ constructor(message) {
92
+ super(message);
93
+ this.name = "PlaycademyError";
94
+ }
95
+ }
96
+
97
+ class ApiError extends Error {
98
+ code;
99
+ details;
100
+ rawBody;
101
+ status;
102
+ constructor(status, code, message, details, rawBody) {
103
+ super(message);
104
+ this.name = "ApiError";
105
+ this.status = status;
106
+ this.code = code;
107
+ this.details = details;
108
+ this.rawBody = rawBody;
109
+ Object.setPrototypeOf(this, ApiError.prototype);
110
+ }
111
+ static fromResponse(status, statusText, body) {
112
+ if (body && typeof body === "object" && "error" in body) {
113
+ const errorBody = body;
114
+ const err = errorBody.error;
115
+ if (err && typeof err === "object") {
116
+ return new ApiError(status, err.code ?? statusCodeToErrorCode(status), err.message ?? statusText, err.details, body);
117
+ }
118
+ }
119
+ return new ApiError(status, statusCodeToErrorCode(status), statusText, undefined, body);
120
+ }
121
+ is(code) {
122
+ return this.code === code;
123
+ }
124
+ isClientError() {
125
+ return this.status >= 400 && this.status < 500;
126
+ }
127
+ isServerError() {
128
+ return this.status >= 500;
129
+ }
130
+ isRetryable() {
131
+ return this.isServerError() || this.code === "TOO_MANY_REQUESTS" || this.code === "RATE_LIMITED" || this.code === "TIMEOUT";
132
+ }
133
+ }
134
+ function statusCodeToErrorCode(status) {
135
+ switch (status) {
136
+ case 400: {
137
+ return "BAD_REQUEST";
138
+ }
139
+ case 401: {
140
+ return "UNAUTHORIZED";
141
+ }
142
+ case 403: {
143
+ return "FORBIDDEN";
144
+ }
145
+ case 404: {
146
+ return "NOT_FOUND";
147
+ }
148
+ case 405: {
149
+ return "METHOD_NOT_ALLOWED";
150
+ }
151
+ case 409: {
152
+ return "CONFLICT";
153
+ }
154
+ case 410: {
155
+ return "GONE";
156
+ }
157
+ case 412: {
158
+ return "PRECONDITION_FAILED";
159
+ }
160
+ case 413: {
161
+ return "PAYLOAD_TOO_LARGE";
162
+ }
163
+ case 422: {
164
+ return "VALIDATION_FAILED";
165
+ }
166
+ case 429: {
167
+ return "TOO_MANY_REQUESTS";
168
+ }
169
+ case 500: {
170
+ return "INTERNAL_ERROR";
171
+ }
172
+ case 501: {
173
+ return "NOT_IMPLEMENTED";
174
+ }
175
+ case 503: {
176
+ return "SERVICE_UNAVAILABLE";
177
+ }
178
+ case 504: {
179
+ return "TIMEOUT";
180
+ }
181
+ default: {
182
+ return status >= 500 ? "INTERNAL_ERROR" : "BAD_REQUEST";
183
+ }
184
+ }
185
+ }
186
+ function extractApiErrorInfo(error) {
187
+ if (!(error instanceof ApiError)) {
188
+ return null;
189
+ }
190
+ return {
191
+ status: error.status,
192
+ code: error.code,
193
+ message: error.message,
194
+ ...error.details !== undefined && { details: error.details }
195
+ };
196
+ }
197
+
198
+ // src/server/request.ts
199
+ async function makeApiRequest(baseUrl, apiToken, endpoint, method = "GET", body) {
200
+ const url = `${baseUrl}${endpoint}`;
201
+ const headers = new Headers;
202
+ headers.set("Content-Type", "application/json");
203
+ headers.set("x-api-key", apiToken);
204
+ const options = {
205
+ method,
206
+ headers
207
+ };
208
+ if (body && (method === "POST" || method === "PUT")) {
209
+ options.body = JSON.stringify(body);
210
+ }
211
+ const response = await fetch(url, options);
212
+ if (!response.ok) {
213
+ let errorBody;
214
+ try {
215
+ errorBody = await response.json();
216
+ } catch {
217
+ try {
218
+ errorBody = await response.text();
219
+ } catch {}
220
+ }
221
+ throw ApiError.fromResponse(response.status, response.statusText, errorBody);
222
+ }
223
+ return await response.json();
224
+ }
225
+
226
+ // src/server/client-base.ts
227
+ class PlaycademyClient {
228
+ state;
229
+ constructor(state) {
230
+ this.state = state;
231
+ }
232
+ static async init(config) {
233
+ const { apiKey, baseUrl, gameId } = config;
234
+ if (!apiKey || typeof apiKey !== "string") {
235
+ throw new Error("[Playcademy SDK] apiKey is required");
236
+ }
237
+ if (!config.config) {
238
+ throw new Error("[Playcademy SDK] config is required in edge environments. " + "Pass config directly, or use PlaycademyClient from @playcademy/sdk/server " + "for filesystem-based config loading.");
239
+ }
240
+ const envBaseUrl = typeof process !== "undefined" ? process.env?.PLAYCADEMY_BASE_URL : undefined;
241
+ const finalBaseUrl = baseUrl || envBaseUrl || "https://hub.playcademy.net";
242
+ const state = {
243
+ apiKey,
244
+ baseUrl: finalBaseUrl,
245
+ gameId: gameId || "",
246
+ config: config.config
247
+ };
248
+ const client = new PlaycademyClient(state);
249
+ if (!gameId) {
250
+ await client.fetchGameId();
251
+ }
252
+ return client;
253
+ }
254
+ async fetchGameId() {
255
+ throw new Error("[Playcademy SDK] gameId is required. Please provide it in the config or implement gameId fetching.");
256
+ }
257
+ async request(path, method = "GET", body) {
258
+ return makeApiRequest(this.state.baseUrl, this.state.apiKey, path, method, body);
259
+ }
260
+ get gameId() {
261
+ return this.state.gameId;
262
+ }
263
+ get config() {
264
+ return this.state.config;
265
+ }
266
+ timeback = createTimebackNamespace(this);
267
+ }
268
+ // src/server/utils/verify-game-token.ts
269
+ async function verifyGameToken(gameToken, options) {
270
+ if (!gameToken || typeof gameToken !== "string") {
271
+ throw new Error("[Playcademy SDK] gameToken must be a non-empty string");
272
+ }
273
+ const baseUrl = options?.baseUrl || process.env.PLAYCADEMY_BASE_URL || process.env.PUBLIC_PLAYCADEMY_BASE_URL || process.env.NEXT_PUBLIC_PLAYCADEMY_BASE_URL;
274
+ if (!baseUrl) {
275
+ throw new Error(`[Playcademy SDK] PLAYCADEMY_BASE_URL is not set
276
+ Please set the PLAYCADEMY_BASE_URL environment variable`);
277
+ }
278
+ try {
279
+ const response = await fetch(`${baseUrl}/api/games/verify`, {
280
+ method: "POST",
281
+ headers: {
282
+ "Content-Type": "application/json"
283
+ },
284
+ body: JSON.stringify({ token: gameToken })
285
+ });
286
+ if (!response.ok) {
287
+ let errorMessage = "Unknown error";
288
+ try {
289
+ const data = await response.json();
290
+ const errorField = data.error || data.message;
291
+ if (typeof errorField === "string") {
292
+ errorMessage = errorField;
293
+ } else if (errorField && typeof errorField === "object") {
294
+ const errorObj = errorField;
295
+ if ("message" in errorObj && typeof errorObj.message === "string") {
296
+ errorMessage = errorObj.message;
297
+ if ("code" in errorObj && typeof errorObj.code === "string") {
298
+ errorMessage = `[${errorObj.code}] ${errorMessage}`;
299
+ }
300
+ } else {
301
+ errorMessage = JSON.stringify(errorField);
302
+ }
303
+ }
304
+ } catch {
305
+ errorMessage = response.statusText || "Unknown error";
306
+ }
307
+ throw new Error(`[Playcademy SDK] Token verification failed: ${response.status} ${errorMessage}`);
308
+ }
309
+ const result = await response.json();
310
+ return result;
311
+ } catch (error) {
312
+ if (error instanceof Error) {
313
+ throw error;
314
+ }
315
+ throw new Error("[Playcademy SDK] Token verification failed: Network error", {
316
+ cause: error
317
+ });
318
+ }
319
+ }
320
+ export {
321
+ verifyGameToken,
322
+ PlaycademyClient
323
+ };
package/dist/server.d.ts CHANGED
@@ -545,6 +545,8 @@ interface BackendDeploymentBundle {
545
545
  bindings?: BackendResourceBindings;
546
546
  /** Optional schema information for database setup */
547
547
  schema?: SchemaInfo;
548
+ /** Optional Cloudflare Worker compatibility flags */
549
+ compatibilityFlags?: string[];
548
550
  }
549
551
 
550
552
  /**
@@ -567,72 +569,39 @@ interface UserInfo {
567
569
  /**
568
570
  * Server-side Playcademy client for recording student activity to TimeBack.
569
571
  *
570
- * This client automatically loads game configuration from playcademy.config.js
571
- * and uses it to auto-fill TimeBack metadata (subject, appName, courseId).
572
+ * This client works in any edge runtime. Config must be provided directly
573
+ * via the `config` option there is no filesystem-based config discovery.
572
574
  *
573
575
  * @example
574
576
  * ```typescript
575
- * // Initialize the client (loads config automatically)
576
577
  * const client = await PlaycademyClient.init({
577
- * apiKey: process.env.PLAYCADEMY_API_KEY
578
+ * apiKey: env.PLAYCADEMY_API_KEY,
579
+ * gameId: env.GAME_ID,
580
+ * config: { name: 'My Game', integrations: { timeback: {...} } }
578
581
  * })
579
582
  *
580
- * // End an activity (metadata auto-filled from config)
581
- * await client.timeback.endActivity(studentId, {
582
- * activityData: { activityId: 'math-quiz-1' },
583
- * scoreData: { correctQuestions: 17, totalQuestions: 20, accuracy: 0.85 },
584
- * timingData: { durationSeconds: 300 },
585
- * xpEarned: 5
586
- * })
583
+ * await client.timeback.endActivity(studentId, { ... })
587
584
  * ```
588
585
  */
589
- declare class PlaycademyClient {
586
+ declare class PlaycademyClient$1 {
590
587
  private state;
591
- /**
592
- * Private constructor. Use PlaycademyClient.init() to create instances.
593
- *
594
- * @param state - Internal client state
595
- * @private
596
- */
597
- private constructor();
588
+ protected constructor(state: PlaycademyServerClientState);
598
589
  /**
599
590
  * Initialize a new PlaycademyClient instance.
600
591
  *
601
- * Loads playcademy.config.js from the current directory (or specified path),
602
- * validates the configuration, and fetches the TimeBack courseId from the API.
592
+ * Requires `config` to be provided directly. For automatic config file
593
+ * discovery from the filesystem, use `PlaycademyClient` from `@playcademy/sdk/server`.
603
594
  *
604
595
  * @param config - Client configuration options
605
596
  * @param config.apiKey - Playcademy API key for authentication
597
+ * @param config.config - Game configuration object (required)
606
598
  * @param config.gameId - Optional game ID (will be fetched from API if not provided)
607
- * @param config.configPath - Optional path to playcademy.config.js (auto-discovered if not provided)
608
599
  * @param config.baseUrl - Optional base URL for Playcademy API (defaults to production)
609
600
  * @returns Promise resolving to initialized PlaycademyClient instance
610
601
  * @throws {Error} If apiKey is missing or invalid
611
- * @throws {Error} If config file cannot be found or loaded
612
- * @throws {Error} If TimeBack integration is not set up for the game
613
- *
614
- * @example
615
- * ```typescript
616
- * // Basic initialization with API key
617
- * const client = await PlaycademyClient.init({
618
- * apiKey: process.env.PLAYCADEMY_API_KEY,
619
- * gameId: 'my-math-game'
620
- * })
621
- *
622
- * // With custom config path
623
- * const client = await PlaycademyClient.init({
624
- * apiKey: process.env.PLAYCADEMY_API_KEY,
625
- * configPath: './custom-config.js'
626
- * })
627
- *
628
- * // With custom base URL (for development)
629
- * const client = await PlaycademyClient.init({
630
- * apiKey: process.env.PLAYCADEMY_API_KEY,
631
- * baseUrl: 'http://localhost:3000'
632
- * })
633
- * ```
602
+ * @throws {Error} If config is not provided
634
603
  */
635
- static init(config: PlaycademyServerClientConfig): Promise<PlaycademyClient>;
604
+ static init(config: PlaycademyServerClientConfig): Promise<PlaycademyClient$1>;
636
605
  private fetchGameId;
637
606
  /**
638
607
  * Makes an authenticated HTTP request to the API.
@@ -670,6 +639,52 @@ declare class PlaycademyClient {
670
639
  };
671
640
  }
672
641
 
642
+ /**
643
+ * @fileoverview Server-side Playcademy Client (Node.js)
644
+ *
645
+ * Extends the edge-safe base client with filesystem-based config discovery.
646
+ * This is the version exported from `@playcademy/sdk/server` — it supports
647
+ * both explicit config and automatic `playcademy.config.js` loading from disk.
648
+ *
649
+ * For edge runtimes without filesystem access, use the base client from
650
+ * `./client-base` (re-exported via `./edge`).
651
+ */
652
+
653
+ /**
654
+ * Server-side Playcademy client for recording student activity to TimeBack.
655
+ *
656
+ * Extends the edge-safe base with automatic playcademy.config.js loading
657
+ * from the filesystem. Works in Node.js and other environments with `fs` access.
658
+ *
659
+ * @example
660
+ * ```typescript
661
+ * // With automatic config discovery (Node.js only)
662
+ * const client = await PlaycademyClient.init({
663
+ * apiKey: process.env.PLAYCADEMY_API_KEY,
664
+ * gameId: 'my-math-game'
665
+ * })
666
+ *
667
+ * // With explicit config (works everywhere)
668
+ * const client = await PlaycademyClient.init({
669
+ * apiKey: process.env.PLAYCADEMY_API_KEY,
670
+ * gameId: 'my-math-game',
671
+ * config: { name: 'My Game', integrations: { timeback: {...} } }
672
+ * })
673
+ * ```
674
+ */
675
+ declare class PlaycademyClient extends PlaycademyClient$1 {
676
+ /**
677
+ * Initialize a new PlaycademyClient instance.
678
+ *
679
+ * If `config` is not provided, loads playcademy.config.js from the current
680
+ * directory (or specified path). This filesystem access requires Node.js.
681
+ *
682
+ * @param config - Client configuration options
683
+ * @returns Promise resolving to initialized PlaycademyClient instance
684
+ */
685
+ static init(config: PlaycademyServerClientConfig): Promise<PlaycademyClient>;
686
+ }
687
+
673
688
  /**
674
689
  * @fileoverview Game Token Verification Utilities
675
690
  *