@playcademy/sdk 0.2.2 → 0.2.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/server.d.ts CHANGED
@@ -1,43 +1,292 @@
1
1
  import { SchemaInfo } from '@playcademy/cloudflare';
2
- import * as _playcademy_timeback_types from '@playcademy/timeback/types';
3
- import { CourseConfig, OrganizationConfig, ComponentConfig, ResourceConfig, ComponentResourceConfig } from '@playcademy/timeback/types';
4
- export { ActivityData, ComponentConfig, ComponentResourceConfig, EndActivityPayload, OrganizationConfig, ResourceConfig, TimebackGrade, TimebackSubject } from '@playcademy/timeback/types';
5
2
 
6
3
  /**
7
- * Basic user information in the shape of the claims from identity providers
4
+ * TimeBack Enums & Literal Types
5
+ *
6
+ * Basic type definitions used throughout the TimeBack integration.
7
+ *
8
+ * @module types/timeback/types
8
9
  */
9
- interface UserInfo {
10
- /** Unique user identifier (sub claim from JWT) */
11
- sub: string;
12
- /** User's email address */
13
- email: string;
14
- /** User's display name */
15
- name: string;
16
- /** Whether the email has been verified */
17
- email_verified: boolean;
18
- /** Optional given name (first name) */
19
- given_name?: string;
20
- /** Optional family name (last name) */
21
- family_name?: string;
22
- /** TimeBack student ID (if user has TimeBack integration) */
23
- timeback_id?: string;
24
- /** Additional user attributes from the identity provider */
10
+ /**
11
+ * Valid TimeBack subject values for course configuration.
12
+ * These are the supported subject values for OneRoster courses.
13
+ */
14
+ type TimebackSubject = 'Reading' | 'Language' | 'Vocabulary' | 'Social Studies' | 'Writing' | 'Science' | 'FastMath' | 'Math' | 'None';
15
+ /**
16
+ * Grade levels per AE OneRoster GradeEnum.
17
+ * -1 = Pre-K, 0 = Kindergarten, 1-12 = Grades 1-12, 13 = AP
18
+ */
19
+ type TimebackGrade = -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13;
20
+ /**
21
+ * Valid Caliper subject values.
22
+ * Matches OneRoster subjects, with "None" as a Caliper-specific fallback.
23
+ */
24
+ type CaliperSubject = 'Reading' | 'Language' | 'Vocabulary' | 'Social Studies' | 'Writing' | 'Science' | 'FastMath' | 'Math' | 'None';
25
+ /**
26
+ * OneRoster organization types.
27
+ */
28
+ type OrganizationType = 'department' | 'school' | 'district' | 'local' | 'state' | 'national';
29
+ /**
30
+ * Lesson types for PowerPath integration.
31
+ */
32
+ type LessonType = 'powerpath-100' | 'quiz' | 'test-out' | 'placement' | 'unit-test' | 'alpha-read-article' | null;
33
+
34
+ /**
35
+ * TimeBack Configuration Types
36
+ *
37
+ * Configuration interfaces for Organization, Course, Component,
38
+ * Resource, and complete TimeBack setup.
39
+ *
40
+ * @module types/timeback/config
41
+ */
42
+
43
+ /**
44
+ * Organization configuration for TimeBack (user input - optionals allowed)
45
+ */
46
+ interface OrganizationConfig {
47
+ /** Display name for your organization */
48
+ name?: string;
49
+ /** Organization type */
50
+ type?: OrganizationType;
51
+ /** Unique identifier (defaults to Playcademy's org) */
52
+ identifier?: string;
53
+ }
54
+ /**
55
+ * Course goals for daily student targets
56
+ */
57
+ interface CourseGoals {
58
+ /** Target XP students should earn per day */
59
+ dailyXp?: number;
60
+ /** Target lessons per day */
61
+ dailyLessons?: number;
62
+ /** Target active minutes per day */
63
+ dailyActiveMinutes?: number;
64
+ /** Target accuracy percentage */
65
+ dailyAccuracy?: number;
66
+ /** Target mastered units per day */
67
+ dailyMasteredUnits?: number;
68
+ }
69
+ /**
70
+ * Course metrics and totals
71
+ */
72
+ interface CourseMetrics {
73
+ /** Total XP available in the course */
74
+ totalXp?: number;
75
+ /** Total lessons/activities in the course */
76
+ totalLessons?: number;
77
+ /** Total number of grade levels covered by this course */
78
+ totalGrades?: number;
79
+ /** The type of course (e.g. 'optional', 'hole-filling', 'base') */
80
+ courseType?: 'base' | 'hole-filling' | 'optional' | 'Base' | 'Hole-Filling' | 'Optional';
81
+ /** Indicates whether the course is supplemental content */
82
+ isSupplemental?: boolean;
83
+ }
84
+ /**
85
+ * Complete course metadata structure
86
+ */
87
+ interface CourseMetadata {
88
+ /** Define the type of course and priority for the student */
89
+ courseType?: 'base' | 'hole-filling' | 'optional';
90
+ /** Boolean value to determine if a course is supplemental to a base course */
91
+ isSupplemental?: boolean;
92
+ /** Boolean value to determine if a course is custom to an individual student */
93
+ isCustom?: boolean;
94
+ /** Signals whether a course is in production with students */
95
+ publishStatus?: 'draft' | 'testing' | 'published' | 'deactivated';
96
+ /** Who to contact when issues reported with questions */
97
+ contactEmail?: string;
98
+ /** Primary app identifier */
99
+ primaryApp?: string;
100
+ /** Learning goals for students */
101
+ goals?: CourseGoals;
102
+ /** Course metrics and totals */
103
+ metrics?: CourseMetrics;
104
+ /** Vendor-specific metadata (e.g., AlphaLearn) */
25
105
  [key: string]: unknown;
26
106
  }
27
107
  /**
28
- * Minimal course configuration for TimeBack integration (used in user-facing config).
108
+ * Course configuration for TimeBack (user input)
109
+ */
110
+ interface CourseConfig {
111
+ /** Allocated OneRoster sourcedId (set after creation) */
112
+ sourcedId?: string;
113
+ /** Course title (defaults to game name) */
114
+ title?: string;
115
+ /** Subjects (REQUIRED for TimeBack integration) */
116
+ subjects: TimebackSubject[];
117
+ /** Used when recording progress/sessions if not explicitly specified per event. */
118
+ defaultSubject?: TimebackSubject;
119
+ /** Grade levels (REQUIRED for TimeBack integration) */
120
+ grades: TimebackGrade[];
121
+ /** Short course code (optional, auto-generated) */
122
+ courseCode?: string;
123
+ /** Course level (auto-derived from grades) */
124
+ level?: 'Elementary' | 'Middle' | 'High' | 'AP' | string;
125
+ /** Grading system */
126
+ gradingScheme?: 'STANDARD';
127
+ /** Total XP available in this course (REQUIRED before setup) */
128
+ totalXp?: number | null;
129
+ /** Total masterable units in this course (REQUIRED before setup) */
130
+ masterableUnits?: number | null;
131
+ /** Custom Playcademy metadata */
132
+ metadata?: CourseMetadata;
133
+ }
134
+ /**
135
+ * Component configuration for TimeBack (user input)
136
+ */
137
+ interface ComponentConfig {
138
+ /** Component title (defaults to "{course.title} Activities") */
139
+ title?: string;
140
+ /** Display order */
141
+ sortOrder?: number;
142
+ /** Required prior components */
143
+ prerequisites?: string[];
144
+ /** How prerequisites work */
145
+ prerequisiteCriteria?: 'ALL' | 'ANY';
146
+ }
147
+ /**
148
+ * Playcademy-specific resource extensions
149
+ */
150
+ interface PlaycademyResourceMetadata {
151
+ /** Mastery configuration for tracking discrete learning units */
152
+ mastery?: {
153
+ /** Total number of masterable units in the resource */
154
+ masterableUnits: number;
155
+ /** Type of mastery unit for semantic clarity */
156
+ unitType?: 'level' | 'rank' | 'skill' | 'module';
157
+ };
158
+ }
159
+ /**
160
+ * Resource configuration for TimeBack (user input)
161
+ */
162
+ interface ResourceConfig {
163
+ /** Resource title (defaults to "{course.title} Game") */
164
+ title?: string;
165
+ /** Internal resource ID (auto-generated from package.json) */
166
+ vendorResourceId?: string;
167
+ /** Vendor identifier */
168
+ vendorId?: string;
169
+ /** Application identifier */
170
+ applicationId?: string;
171
+ /** Resource roles */
172
+ roles?: ('primary' | 'secondary')[];
173
+ /** Resource importance */
174
+ importance?: 'primary' | 'secondary';
175
+ /** Interactive resource metadata */
176
+ metadata?: {
177
+ /** Resource type */
178
+ type?: 'interactive';
179
+ /** Launch URL (defaults to Playcademy game URL) */
180
+ launchUrl?: string;
181
+ /** Platform name */
182
+ toolProvider?: string;
183
+ /** Teaching method */
184
+ instructionalMethod?: 'exploratory' | 'direct-instruction';
185
+ /** Subject area */
186
+ subject?: TimebackSubject;
187
+ /** Target grades */
188
+ grades?: TimebackGrade[];
189
+ /** Content language */
190
+ language?: string;
191
+ /** Base XP for completion */
192
+ xp?: number;
193
+ /** Playcademy-specific extensions */
194
+ playcademy?: PlaycademyResourceMetadata;
195
+ };
196
+ }
197
+ /**
198
+ * Component Resource link configuration (user input)
199
+ */
200
+ interface ComponentResourceConfig {
201
+ /** Link title (defaults to "{resource.title} Activity") */
202
+ title?: string;
203
+ /** Display order */
204
+ sortOrder?: number;
205
+ /** Lesson type for PowerPath integration */
206
+ lessonType?: LessonType;
207
+ }
208
+ interface TimebackCourseConfig {
209
+ subject: string;
210
+ grade: number;
211
+ }
212
+
213
+ /**
214
+ * TimeBack Client SDK DTOs
29
215
  *
30
- * NOTE: Per-course overrides (title, courseCode, level, metadata) are defined
31
- * in @playcademy/sdk/server as TimebackCourseConfigWithOverrides.
32
- * This base type only includes the minimal required fields.
216
+ * Data transfer objects for the TimeBack client SDK including
217
+ * progress tracking, session management, and activity completion.
33
218
  *
34
- * For totalXp, use metadata.metrics.totalXp (aligns with upstream TimeBack structure).
219
+ * Note: TimebackClientConfig lives in @playcademy/timeback as it's
220
+ * SDK configuration, not a DTO.
221
+ *
222
+ * @module types/timeback/client
35
223
  */
36
- type TimebackCourseConfig = {
37
- subject: string;
224
+
225
+ /**
226
+ * Activity data for ending an activity
227
+ */
228
+ interface ActivityData {
229
+ /** Unique activity identifier (required) */
230
+ activityId: string;
231
+ /** Grade level for this activity (required for multi-grade course routing) */
38
232
  grade: number;
39
- };
40
- type EndActivityResponse = {
233
+ /** Subject area (required for multi-grade course routing) */
234
+ subject: CaliperSubject;
235
+ /** Activity display name (optional) */
236
+ activityName?: string;
237
+ /** Course identifier (auto-filled from config if not provided) */
238
+ courseId?: string;
239
+ /** Course display name (auto-filled from config if not provided) */
240
+ courseName?: string;
241
+ /** Student email address (optional) */
242
+ studentEmail?: string;
243
+ /** Application name for Caliper events (defaults to 'Game') */
244
+ appName?: string;
245
+ /** Sensor URL for Caliper events (defaults to baseUrl) */
246
+ sensorUrl?: string;
247
+ }
248
+ /**
249
+ * Score data for activity completion
250
+ */
251
+ interface ScoreData {
252
+ /** Number of questions answered correctly */
253
+ correctQuestions: number;
254
+ /** Total number of questions */
255
+ totalQuestions: number;
256
+ }
257
+ /**
258
+ * Timing data for activity completion
259
+ */
260
+ interface TimingData {
261
+ /** Duration of the activity in seconds */
262
+ durationSeconds: number;
263
+ }
264
+ /**
265
+ * Complete payload for ending an activity
266
+ */
267
+ interface EndActivityPayload {
268
+ /** Activity metadata */
269
+ activityData: ActivityData;
270
+ /** Score information */
271
+ scoreData: ScoreData;
272
+ /** Timing information */
273
+ timingData: TimingData;
274
+ /** Explicit XP value to override automatic calculation */
275
+ xpEarned?: number;
276
+ /** Number of learning units mastered */
277
+ masteredUnits?: number;
278
+ }
279
+
280
+ /**
281
+ * TimeBack API Request/Response Types
282
+ *
283
+ * Types for TimeBack API endpoints including XP tracking,
284
+ * setup, verification, and activity completion.
285
+ *
286
+ * @module types/timeback/api
287
+ */
288
+
289
+ interface EndActivityResponse {
41
290
  status: 'ok';
42
291
  courseId: string;
43
292
  xpAwarded: number;
@@ -45,7 +294,7 @@ type EndActivityResponse = {
45
294
  pctCompleteApp?: number;
46
295
  scoreStatus?: string;
47
296
  inProgress?: string;
48
- };
297
+ }
49
298
 
50
299
  /**
51
300
  * @fileoverview Server SDK Type Definitions
@@ -253,6 +502,23 @@ interface BackendDeploymentBundle {
253
502
  secrets?: Record<string, string>;
254
503
  }
255
504
 
505
+ /**
506
+ * OpenID Connect UserInfo claims (NOT a database row).
507
+ */
508
+ interface UserInfo {
509
+ sub: string;
510
+ email: string;
511
+ name: string | null;
512
+ email_verified?: boolean;
513
+ given_name?: string;
514
+ family_name?: string;
515
+ issuer?: string;
516
+ lti_roles?: unknown;
517
+ lti_context?: unknown;
518
+ lti_resource_link?: unknown;
519
+ timeback_id?: string;
520
+ }
521
+
256
522
  /**
257
523
  * Server-side Playcademy client for recording student activity to TimeBack.
258
524
  *
@@ -322,13 +588,6 @@ declare class PlaycademyClient {
322
588
  * ```
323
589
  */
324
590
  static init(config: PlaycademyServerClientConfig): Promise<PlaycademyClient>;
325
- /**
326
- * Fetch gameId from API using the API token.
327
- *
328
- * @private
329
- * @throws {Error} Always throws - gameId fetching not yet implemented
330
- * @todo Implement API endpoint to fetch gameId from API token
331
- */
332
591
  private fetchGameId;
333
592
  /**
334
593
  * Makes an authenticated HTTP request to the API.
@@ -357,7 +616,7 @@ declare class PlaycademyClient {
357
616
  get config(): PlaycademyServerClientState['config'];
358
617
  /** TimeBack integration methods (endActivity) */
359
618
  timeback: {
360
- endActivity: (studentId: string, payload: _playcademy_timeback_types.EndActivityPayload) => Promise<EndActivityResponse>;
619
+ endActivity: (studentId: string, payload: EndActivityPayload) => Promise<EndActivityResponse>;
361
620
  };
362
621
  }
363
622
 
@@ -420,4 +679,4 @@ declare function verifyGameToken(gameToken: string, options?: {
420
679
  }): Promise<VerifyGameTokenResponse>;
421
680
 
422
681
  export { PlaycademyClient, verifyGameToken };
423
- export type { BackendDeploymentBundle, BackendResourceBindings, IntegrationsConfig, PlaycademyConfig, PlaycademyServerClientConfig, PlaycademyServerClientState, TimebackBaseConfig, TimebackCourseConfigWithOverrides, TimebackIntegrationConfig, UserInfo };
682
+ export type { ActivityData, BackendDeploymentBundle, BackendResourceBindings, ComponentConfig, ComponentResourceConfig, EndActivityPayload, IntegrationsConfig, OrganizationConfig, PlaycademyConfig, PlaycademyServerClientConfig, PlaycademyServerClientState, ResourceConfig, TimebackBaseConfig, TimebackCourseConfigWithOverrides, TimebackGrade, TimebackIntegrationConfig, TimebackSubject, UserInfo };
package/dist/server.js CHANGED
@@ -27,6 +27,99 @@ function createTimebackNamespace(client) {
27
27
  }
28
28
  };
29
29
  }
30
+ // src/core/errors.ts
31
+ class PlaycademyError extends Error {
32
+ constructor(message) {
33
+ super(message);
34
+ this.name = "PlaycademyError";
35
+ }
36
+ }
37
+
38
+ class ApiError extends Error {
39
+ status;
40
+ code;
41
+ details;
42
+ rawBody;
43
+ constructor(status, code, message, details, rawBody) {
44
+ super(message);
45
+ this.status = status;
46
+ this.name = "ApiError";
47
+ this.code = code;
48
+ this.details = details;
49
+ this.rawBody = rawBody;
50
+ Object.setPrototypeOf(this, ApiError.prototype);
51
+ }
52
+ static fromResponse(status, statusText, body) {
53
+ if (body && typeof body === "object" && "error" in body) {
54
+ const errorBody = body;
55
+ const err = errorBody.error;
56
+ if (err && typeof err === "object") {
57
+ return new ApiError(status, err.code ?? statusCodeToErrorCode(status), err.message ?? statusText, err.details, body);
58
+ }
59
+ }
60
+ return new ApiError(status, statusCodeToErrorCode(status), statusText, undefined, body);
61
+ }
62
+ is(code) {
63
+ return this.code === code;
64
+ }
65
+ isClientError() {
66
+ return this.status >= 400 && this.status < 500;
67
+ }
68
+ isServerError() {
69
+ return this.status >= 500;
70
+ }
71
+ isRetryable() {
72
+ return this.isServerError() || this.code === "TOO_MANY_REQUESTS" || this.code === "RATE_LIMITED" || this.code === "TIMEOUT";
73
+ }
74
+ }
75
+ function statusCodeToErrorCode(status) {
76
+ switch (status) {
77
+ case 400:
78
+ return "BAD_REQUEST";
79
+ case 401:
80
+ return "UNAUTHORIZED";
81
+ case 403:
82
+ return "FORBIDDEN";
83
+ case 404:
84
+ return "NOT_FOUND";
85
+ case 405:
86
+ return "METHOD_NOT_ALLOWED";
87
+ case 409:
88
+ return "CONFLICT";
89
+ case 410:
90
+ return "GONE";
91
+ case 412:
92
+ return "PRECONDITION_FAILED";
93
+ case 413:
94
+ return "PAYLOAD_TOO_LARGE";
95
+ case 422:
96
+ return "VALIDATION_FAILED";
97
+ case 429:
98
+ return "TOO_MANY_REQUESTS";
99
+ case 500:
100
+ return "INTERNAL_ERROR";
101
+ case 501:
102
+ return "NOT_IMPLEMENTED";
103
+ case 503:
104
+ return "SERVICE_UNAVAILABLE";
105
+ case 504:
106
+ return "TIMEOUT";
107
+ default:
108
+ return status >= 500 ? "INTERNAL_ERROR" : "BAD_REQUEST";
109
+ }
110
+ }
111
+ function extractApiErrorInfo(error) {
112
+ if (!(error instanceof ApiError)) {
113
+ return null;
114
+ }
115
+ return {
116
+ status: error.status,
117
+ code: error.code,
118
+ message: error.message,
119
+ ...error.details !== undefined && { details: error.details }
120
+ };
121
+ }
122
+
30
123
  // src/server/request.ts
31
124
  async function makeApiRequest(baseUrl, apiToken, endpoint, method = "GET", body) {
32
125
  const url = `${baseUrl}${endpoint}`;
@@ -40,16 +133,19 @@ async function makeApiRequest(baseUrl, apiToken, endpoint, method = "GET", body)
40
133
  if (body && (method === "POST" || method === "PUT")) {
41
134
  options.body = JSON.stringify(body);
42
135
  }
43
- try {
44
- const response = await fetch(url, options);
45
- if (!response.ok) {
46
- const errorText = await response.text().catch(() => "Unknown error");
47
- throw new Error(`API request failed: ${response.status} ${errorText}`);
136
+ const response = await fetch(url, options);
137
+ if (!response.ok) {
138
+ let errorBody;
139
+ try {
140
+ errorBody = await response.json();
141
+ } catch {
142
+ try {
143
+ errorBody = await response.text();
144
+ } catch {}
48
145
  }
49
- return await response.json();
50
- } catch (error) {
51
- throw new Error(`Request failed: ${error instanceof Error ? error.message : String(error)}`);
146
+ throw ApiError.fromResponse(response.status, response.statusText, errorBody);
52
147
  }
148
+ return await response.json();
53
149
  }
54
150
 
55
151
  // src/server/utils/config-loader.ts