playcademy 0.22.1 → 0.22.2-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.
package/dist/cli.js CHANGED
@@ -1065,7 +1065,7 @@ var SAMPLE_BUCKET_FILENAME = "bucket.ts";
1065
1065
  // ../better-auth/package.json
1066
1066
  var package_default = {
1067
1067
  name: "@playcademy/better-auth",
1068
- version: "0.0.14",
1068
+ version: "0.0.15-beta.2",
1069
1069
  type: "module",
1070
1070
  exports: {
1071
1071
  "./server": {
@@ -1262,6 +1262,17 @@ var TIMEBACK_COMPONENT_RESOURCE_DEFAULTS = {
1262
1262
  sortOrder: 1,
1263
1263
  lessonType: "quiz"
1264
1264
  };
1265
+ var TIMEBACK_GAME_METRIC_DECIMAL_PLACES = {
1266
+ xp: 1,
1267
+ mastery: 0,
1268
+ score: 2
1269
+ };
1270
+ var TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE = {
1271
+ xp: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.xp,
1272
+ mastery: 0,
1273
+ time: 60,
1274
+ score: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.score
1275
+ };
1265
1276
 
1266
1277
  // ../constants/src/cloudflare.ts
1267
1278
  var WORKER_NAMING = {
@@ -2909,7 +2920,7 @@ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as re
2909
2920
  import { join as join13 } from "node:path";
2910
2921
 
2911
2922
  // src/version.ts
2912
- var cliVersion = false ? "0.0.0-dev" : "0.22.1";
2923
+ var cliVersion = false ? "0.0.0-dev" : "0.22.2-beta.2";
2913
2924
 
2914
2925
  // src/lib/init/database.ts
2915
2926
  var drizzleConfigTemplate = loadTemplateString("database/drizzle-config.ts");
package/dist/constants.js CHANGED
@@ -20,7 +20,7 @@ var SAMPLE_BUCKET_FILENAME = "bucket.ts";
20
20
  // ../better-auth/package.json
21
21
  var package_default = {
22
22
  name: "@playcademy/better-auth",
23
- version: "0.0.14",
23
+ version: "0.0.15-beta.2",
24
24
  type: "module",
25
25
  exports: {
26
26
  "./server": {
@@ -247,6 +247,19 @@ var GAME_WORKER_DOMAINS = {
247
247
  staging: "staging.playcademy.gg"
248
248
  };
249
249
 
250
+ // ../constants/src/timeback.ts
251
+ var TIMEBACK_GAME_METRIC_DECIMAL_PLACES = {
252
+ xp: 1,
253
+ mastery: 0,
254
+ score: 2
255
+ };
256
+ var TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE = {
257
+ xp: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.xp,
258
+ mastery: 0,
259
+ time: 60,
260
+ score: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.score
261
+ };
262
+
250
263
  // src/constants/urls.ts
251
264
  var DOCS_URL = "https://docs.dev.playcademy.net/platform";
252
265
  export {
package/dist/db.js CHANGED
@@ -36,7 +36,7 @@ var DEFAULT_API_ROUTES_DIRECTORY = join2(SERVER_ROOT_DIRECTORY, "api");
36
36
  // ../better-auth/package.json
37
37
  var package_default = {
38
38
  name: "@playcademy/better-auth",
39
- version: "0.0.14",
39
+ version: "0.0.15-beta.2",
40
40
  type: "module",
41
41
  exports: {
42
42
  "./server": {
@@ -147,6 +147,19 @@ var CLI_FILES = {
147
147
  INITIAL_DATABASE: "initial.sqlite"
148
148
  };
149
149
 
150
+ // ../constants/src/timeback.ts
151
+ var TIMEBACK_GAME_METRIC_DECIMAL_PLACES = {
152
+ xp: 1,
153
+ mastery: 0,
154
+ score: 2
155
+ };
156
+ var TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE = {
157
+ xp: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.xp,
158
+ mastery: 0,
159
+ time: 60,
160
+ score: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.score
161
+ };
162
+
150
163
  // src/lib/db/path.ts
151
164
  var DB_DIRECTORY = CLI_DIRECTORIES.DATABASE;
152
165
  var INITIAL_DB_NAME = CLI_FILES.INITIAL_DATABASE;
package/dist/index.js CHANGED
@@ -326,7 +326,7 @@ var SAMPLE_BUCKET_FILENAME = "bucket.ts";
326
326
  // ../better-auth/package.json
327
327
  var package_default = {
328
328
  name: "@playcademy/better-auth",
329
- version: "0.0.14",
329
+ version: "0.0.15-beta.2",
330
330
  type: "module",
331
331
  exports: {
332
332
  "./server": {
@@ -534,6 +534,7 @@ var LOG_COLLECTOR_URL = "https://logs.playcademy.gg";
534
534
  var TIMEBACK_ROUTES = {
535
535
  END_ACTIVITY: "/integrations/timeback/end-activity",
536
536
  GET_XP: "/integrations/timeback/xp",
537
+ GET_MASTERY: "/integrations/timeback/mastery",
537
538
  HEARTBEAT: "/integrations/timeback/heartbeat",
538
539
  ADVANCE_COURSE: "/integrations/timeback/advance-course"
539
540
  };
@@ -576,6 +577,17 @@ var TIMEBACK_COMPONENT_RESOURCE_DEFAULTS = {
576
577
  sortOrder: 1,
577
578
  lessonType: "quiz"
578
579
  };
580
+ var TIMEBACK_GAME_METRIC_DECIMAL_PLACES = {
581
+ xp: 1,
582
+ mastery: 0,
583
+ score: 2
584
+ };
585
+ var TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE = {
586
+ xp: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.xp,
587
+ mastery: 0,
588
+ time: 60,
589
+ score: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.score
590
+ };
579
591
 
580
592
  // ../constants/src/cloudflare.ts
581
593
  var WORKER_NAMING = {
@@ -4171,7 +4183,7 @@ import { existsSync as existsSync9, mkdirSync as mkdirSync2, readFileSync as rea
4171
4183
  import { join as join13 } from "node:path";
4172
4184
 
4173
4185
  // src/version.ts
4174
- var cliVersion = false ? "0.0.0-dev" : "0.22.1";
4186
+ var cliVersion = false ? "0.0.0-dev" : "0.22.2-beta.2";
4175
4187
 
4176
4188
  // src/lib/init/database.ts
4177
4189
  var drizzleConfigTemplate = loadTemplateString("database/drizzle-config.ts");
@@ -6612,6 +6624,7 @@ var ROUTES = {
6612
6624
  TIMEBACK: {
6613
6625
  END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}`,
6614
6626
  GET_XP: `/api${TIMEBACK_ROUTES.GET_XP}`,
6627
+ GET_MASTERY: `/api${TIMEBACK_ROUTES.GET_MASTERY}`,
6615
6628
  HEARTBEAT: `/api${TIMEBACK_ROUTES.HEARTBEAT}`,
6616
6629
  ADVANCE_COURSE: `/api${TIMEBACK_ROUTES.ADVANCE_COURSE}`
6617
6630
  }
@@ -8,6 +8,75 @@ var __export = (target, all) => {
8
8
  __defProp(target, name, { get: all[name], enumerable: true });
9
9
  };
10
10
 
11
+ // ../utils/src/timeback.ts
12
+ function formatGradeLabel(grade) {
13
+ switch (grade) {
14
+ case -1: {
15
+ return "Pre-K";
16
+ }
17
+ case 0: {
18
+ return "Kindergarten";
19
+ }
20
+ case 13: {
21
+ return "AP";
22
+ }
23
+ default: {
24
+ return `Grade ${grade}`;
25
+ }
26
+ }
27
+ }
28
+ var init_timeback = __esm({
29
+ "../utils/src/timeback.ts"() {
30
+ }
31
+ });
32
+
33
+ // ../edge-play/src/lib/validation.ts
34
+ function isNonNegativeNumber(value) {
35
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
36
+ }
37
+ function isNonNegativeInteger(value) {
38
+ return isNonNegativeNumber(value) && Number.isInteger(value);
39
+ }
40
+ function isValidGrade(value) {
41
+ return typeof value === "number" && Number.isInteger(value) && VALID_GRADES.includes(value);
42
+ }
43
+ function isValidSubject(value) {
44
+ return typeof value === "string" && VALID_SUBJECTS.includes(value);
45
+ }
46
+ function validateCourseConfig(params) {
47
+ const { grade, subject, config } = params;
48
+ const timebackConfig = config.integrations?.timeback;
49
+ const configuredCourse = timebackConfig?.courses?.find(
50
+ (course) => course.grade === grade && course.subject === subject
51
+ );
52
+ if (!configuredCourse) {
53
+ const configured = timebackConfig?.courses?.map((c) => `${c.subject} (${formatGradeLabel(c.grade)})`).join(", ");
54
+ return {
55
+ error: `Invalid grade/subject combination: ${subject} (${formatGradeLabel(grade)}). Configured courses: ${configured || "none"}`
56
+ };
57
+ }
58
+ return null;
59
+ }
60
+ var VALID_GRADES, VALID_SUBJECTS;
61
+ var init_validation = __esm({
62
+ "../edge-play/src/lib/validation.ts"() {
63
+ "use strict";
64
+ init_timeback();
65
+ VALID_GRADES = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
66
+ VALID_SUBJECTS = [
67
+ "Reading",
68
+ "Language",
69
+ "Vocabulary",
70
+ "Social Studies",
71
+ "Writing",
72
+ "Science",
73
+ "FastMath",
74
+ "Math",
75
+ "None"
76
+ ];
77
+ }
78
+ });
79
+
11
80
  // ../constants/src/auth.ts
12
81
  var init_auth = __esm({
13
82
  "../constants/src/auth.ts"() {
@@ -67,16 +136,28 @@ var init_system = __esm({
67
136
  });
68
137
 
69
138
  // ../constants/src/timeback.ts
70
- var TIMEBACK_ROUTES;
71
- var init_timeback = __esm({
139
+ var TIMEBACK_ROUTES, TIMEBACK_GAME_METRIC_DECIMAL_PLACES, TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE;
140
+ var init_timeback2 = __esm({
72
141
  "../constants/src/timeback.ts"() {
73
142
  "use strict";
74
143
  TIMEBACK_ROUTES = {
75
144
  END_ACTIVITY: "/integrations/timeback/end-activity",
76
145
  GET_XP: "/integrations/timeback/xp",
146
+ GET_MASTERY: "/integrations/timeback/mastery",
77
147
  HEARTBEAT: "/integrations/timeback/heartbeat",
78
148
  ADVANCE_COURSE: "/integrations/timeback/advance-course"
79
149
  };
150
+ TIMEBACK_GAME_METRIC_DECIMAL_PLACES = {
151
+ xp: 1,
152
+ mastery: 0,
153
+ score: 2
154
+ };
155
+ TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE = {
156
+ xp: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.xp,
157
+ mastery: 0,
158
+ time: 60,
159
+ score: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.score
160
+ };
80
161
  }
81
162
  });
82
163
 
@@ -100,7 +181,7 @@ var init_src = __esm({
100
181
  init_game();
101
182
  init_platform();
102
183
  init_system();
103
- init_timeback();
184
+ init_timeback2();
104
185
  init_cloudflare();
105
186
  }
106
187
  });
@@ -120,6 +201,7 @@ var init_constants = __esm({
120
201
  TIMEBACK: {
121
202
  END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}`,
122
203
  GET_XP: `/api${TIMEBACK_ROUTES.GET_XP}`,
204
+ GET_MASTERY: `/api${TIMEBACK_ROUTES.GET_MASTERY}`,
123
205
  HEARTBEAT: `/api${TIMEBACK_ROUTES.HEARTBEAT}`,
124
206
  ADVANCE_COURSE: `/api${TIMEBACK_ROUTES.ADVANCE_COURSE}`
125
207
  }
@@ -212,9 +294,9 @@ var init_routes = __esm({
212
294
  // ../edge-play/src/entry/metadata.ts
213
295
  function getRuntimeMetadata() {
214
296
  return {
215
- cliVersion: true ? "0.22.1" : "0.0.0-dev",
216
- sdkVersion: true ? "0.10.0" : "0.0.0-dev",
217
- buildId: true ? "86c338e2c882" : "dev-source"
297
+ cliVersion: true ? "0.22.2-beta.2" : "0.0.0-dev",
298
+ sdkVersion: true ? "0.10.1-beta.2" : "0.0.0-dev",
299
+ buildId: true ? "9c6fe529d1f4" : "dev-source"
218
300
  };
219
301
  }
220
302
  var init_metadata = __esm({
@@ -364,69 +446,6 @@ var init_errors = __esm({
364
446
  }
365
447
  });
366
448
 
367
- // ../utils/src/timeback.ts
368
- function formatGradeLabel(grade) {
369
- switch (grade) {
370
- case -1: {
371
- return "Pre-K";
372
- }
373
- case 0: {
374
- return "Kindergarten";
375
- }
376
- case 13: {
377
- return "AP";
378
- }
379
- default: {
380
- return `Grade ${grade}`;
381
- }
382
- }
383
- }
384
- var init_timeback2 = __esm({
385
- "../utils/src/timeback.ts"() {
386
- }
387
- });
388
-
389
- // ../edge-play/src/lib/validation.ts
390
- function isValidGrade2(value) {
391
- return typeof value === "number" && Number.isInteger(value) && VALID_GRADES2.includes(value);
392
- }
393
- function isValidSubject2(value) {
394
- return typeof value === "string" && VALID_SUBJECTS2.includes(value);
395
- }
396
- function validateCourseConfig(params) {
397
- const { grade, subject, config } = params;
398
- const timebackConfig = config.integrations?.timeback;
399
- const configuredCourse = timebackConfig?.courses?.find(
400
- (course) => course.grade === grade && course.subject === subject
401
- );
402
- if (!configuredCourse) {
403
- const configured = timebackConfig?.courses?.map((c) => `${c.subject} (${formatGradeLabel(c.grade)})`).join(", ");
404
- return {
405
- error: `Invalid grade/subject combination: ${subject} (${formatGradeLabel(grade)}). Configured courses: ${configured || "none"}`
406
- };
407
- }
408
- return null;
409
- }
410
- var VALID_GRADES2, VALID_SUBJECTS2;
411
- var init_validation = __esm({
412
- "../edge-play/src/lib/validation.ts"() {
413
- "use strict";
414
- init_timeback2();
415
- VALID_GRADES2 = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
416
- VALID_SUBJECTS2 = [
417
- "Reading",
418
- "Language",
419
- "Vocabulary",
420
- "Social Studies",
421
- "Writing",
422
- "Science",
423
- "FastMath",
424
- "Math",
425
- "None"
426
- ];
427
- }
428
- });
429
-
430
449
  // ../edge-play/src/lib/index.ts
431
450
  var init_lib = __esm({
432
451
  "../edge-play/src/lib/index.ts"() {
@@ -495,12 +514,13 @@ async function POST(c) {
495
514
  sessionTimingData,
496
515
  xpEarned,
497
516
  masteredUnits,
517
+ masteredUnitsAbsolute,
498
518
  extensions
499
519
  } = await c.req.json();
500
520
  assertTimebackIntegrated(c);
501
521
  const config = getConfig(c);
502
522
  const structuredActivityData = isActivityDataInput(activityData) ? activityData : void 0;
503
- if (structuredActivityData && isValidGrade2(structuredActivityData.grade) && isValidSubject2(structuredActivityData.subject)) {
523
+ if (structuredActivityData && isValidGrade(structuredActivityData.grade) && isValidSubject(structuredActivityData.subject)) {
504
524
  const courseValidationError = validateCourseConfig({
505
525
  grade: structuredActivityData.grade,
506
526
  subject: structuredActivityData.subject,
@@ -532,6 +552,7 @@ async function POST(c) {
532
552
  sessionTimingData,
533
553
  xpEarned,
534
554
  masteredUnits,
555
+ masteredUnitsAbsolute,
535
556
  extensions
536
557
  }
537
558
  );
@@ -581,12 +602,12 @@ async function GET3(c) {
581
602
  }
582
603
  grade = parseInt(gradeParam, 10);
583
604
  subject = subjectParam;
584
- if (!isValidGrade2(grade)) {
585
- const message2 = `grade must be a valid grade level (${VALID_GRADES2.join(", ")})`;
605
+ if (!isValidGrade(grade)) {
606
+ const message2 = `grade must be a valid grade level (${VALID_GRADES.join(", ")})`;
586
607
  return c.json(buildErrorResponse(c, message2, message2), 400);
587
608
  }
588
- if (!isValidSubject2(subject)) {
589
- const message2 = `subject must be a valid subject (${VALID_SUBJECTS2.join(", ")})`;
609
+ if (!isValidSubject(subject)) {
610
+ const message2 = `subject must be a valid subject (${VALID_SUBJECTS.join(", ")})`;
590
611
  return c.json(buildErrorResponse(c, message2, message2), 400);
591
612
  }
592
613
  const courseValidationError = validateCourseConfig({ grade, subject, config });
@@ -617,6 +638,74 @@ var init_get_xp = __esm({
617
638
  }
618
639
  });
619
640
 
641
+ // ../edge-play/src/routes/integrations/timeback/get-mastery.ts
642
+ var get_mastery_exports = {};
643
+ __export(get_mastery_exports, {
644
+ GET: () => GET4
645
+ });
646
+ async function GET4(c) {
647
+ try {
648
+ const user = c.get("playcademyUser");
649
+ if (!user) {
650
+ return c.json(buildErrorResponse(c, "Unauthorized", "Unauthorized"), 401);
651
+ }
652
+ if (!user.timeback_id) {
653
+ const message2 = "User does not have TimeBack integration";
654
+ console.error("[TimeBack Get Mastery] Error:", message2);
655
+ return c.json(buildErrorResponse(c, message2, message2), 400);
656
+ }
657
+ assertTimebackIntegrated(c);
658
+ const config = getConfig(c);
659
+ const url = new URL(c.req.url);
660
+ const gradeParam = url.searchParams.get("grade");
661
+ const subjectParam = url.searchParams.get("subject");
662
+ const includeParam = url.searchParams.get("include");
663
+ const include = includeParam ? includeParam.split(",").map((s) => s.trim()) : void 0;
664
+ let grade;
665
+ let subject;
666
+ if (gradeParam !== null || subjectParam !== null) {
667
+ if (gradeParam === null || subjectParam === null) {
668
+ const message2 = "Both grade and subject must be provided together";
669
+ return c.json(buildErrorResponse(c, message2, message2), 400);
670
+ }
671
+ grade = parseInt(gradeParam, 10);
672
+ subject = subjectParam;
673
+ if (!isValidGrade(grade)) {
674
+ const message2 = `grade must be a valid grade level (${VALID_GRADES.join(", ")})`;
675
+ return c.json(buildErrorResponse(c, message2, message2), 400);
676
+ }
677
+ if (!isValidSubject(subject)) {
678
+ const message2 = `subject must be a valid subject (${VALID_SUBJECTS.join(", ")})`;
679
+ return c.json(buildErrorResponse(c, message2, message2), 400);
680
+ }
681
+ const courseValidationError = validateCourseConfig({ grade, subject, config });
682
+ if (courseValidationError) {
683
+ return c.json(
684
+ buildErrorResponse(c, courseValidationError.error, courseValidationError.error),
685
+ 400
686
+ );
687
+ }
688
+ }
689
+ const sdk = c.get("sdk");
690
+ const result = await sdk.timeback.getStudentMastery(user.timeback_id, {
691
+ grade,
692
+ subject,
693
+ include
694
+ });
695
+ return c.json(result);
696
+ } catch (error) {
697
+ logError("TimeBack Get Mastery", error);
698
+ return c.json(buildErrorResponse(c, error, "Failed to get mastery"), getErrorStatus(error));
699
+ }
700
+ }
701
+ var init_get_mastery = __esm({
702
+ "../edge-play/src/routes/integrations/timeback/get-mastery.ts"() {
703
+ "use strict";
704
+ init_lib();
705
+ init_shared();
706
+ }
707
+ });
708
+
620
709
  // ../edge-play/src/routes/integrations/timeback/heartbeat.ts
621
710
  var heartbeat_exports = {};
622
711
  __export(heartbeat_exports, {
@@ -652,7 +741,7 @@ async function POST2(c) {
652
741
  assertTimebackIntegrated(c);
653
742
  const config = getConfig(c);
654
743
  const structuredActivityData = isActivityDataInput(activityData) ? activityData : void 0;
655
- if (structuredActivityData && isValidGrade2(structuredActivityData.grade) && isValidSubject2(structuredActivityData.subject)) {
744
+ if (structuredActivityData && isValidGrade(structuredActivityData.grade) && isValidSubject(structuredActivityData.subject)) {
656
745
  const courseValidationError = validateCourseConfig({
657
746
  grade: structuredActivityData.grade,
658
747
  subject: structuredActivityData.subject,
@@ -716,7 +805,7 @@ async function POST3(c) {
716
805
  }
717
806
  const body = await c.req.json().catch(() => ({}));
718
807
  const subject = body.subject;
719
- if (subject !== void 0 && !isValidSubject2(subject)) {
808
+ if (subject !== void 0 && !isValidSubject(subject)) {
720
809
  const message2 = "Invalid subject: must be one of the supported TimeBack subjects";
721
810
  console.error("[TimeBack Advance] Error:", message2);
722
811
  return c.json(buildErrorResponse(c, message2, message2), 400);
@@ -3484,22 +3573,10 @@ function createLocalJWKSet(jwks) {
3484
3573
  return localJWKSet;
3485
3574
  }
3486
3575
 
3487
- // ../edge-play/src/entry/metrics.ts
3576
+ // ../edge-play/src/lib/jwt.ts
3488
3577
  var SERVICE_JWT_ISSUER = "playcademy-platform";
3489
3578
  var SERVICE_JWT_AUDIENCE = "playcademy-game-metrics";
3490
3579
  var SERVICE_JWT_CLOCK_TOLERANCE = "10s";
3491
- var TIMEBACK_GRADES = /* @__PURE__ */ new Set([-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]);
3492
- var TIMEBACK_SUBJECTS = /* @__PURE__ */ new Set([
3493
- "Reading",
3494
- "Language",
3495
- "Vocabulary",
3496
- "Social Studies",
3497
- "Writing",
3498
- "Science",
3499
- "FastMath",
3500
- "Math",
3501
- "None"
3502
- ]);
3503
3580
  function getBearerToken(c) {
3504
3581
  const header = c.req.header("Authorization");
3505
3582
  if (!header?.startsWith("Bearer ")) {
@@ -3594,22 +3671,24 @@ async function verifyMetricsToken(c) {
3594
3671
  return null;
3595
3672
  }
3596
3673
  }
3597
- function isNonNegativeNumber(value) {
3598
- return typeof value === "number" && Number.isFinite(value) && value >= 0;
3599
- }
3600
- function isNonNegativeInteger(value) {
3601
- return isNonNegativeNumber(value) && Number.isInteger(value);
3602
- }
3603
- function isIsoDateString(value) {
3604
- return typeof value === "string" && !Number.isNaN(Date.parse(value));
3674
+
3675
+ // ../utils/src/uuid.ts
3676
+ var UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
3677
+ function isValidUUID(value) {
3678
+ if (!value || typeof value !== "string") {
3679
+ return false;
3680
+ }
3681
+ return UUID_REGEX.test(value);
3605
3682
  }
3606
- function isValidActivity(value) {
3683
+
3684
+ // ../edge-play/src/lib/metrics.ts
3685
+ init_validation();
3686
+ function isValidRunMetrics(value) {
3607
3687
  if (!value || typeof value !== "object") {
3608
3688
  return false;
3609
3689
  }
3610
3690
  const activity = value;
3611
- const completionCount = activity.completionCount;
3612
- return typeof activity.activityId === "string" && activity.activityId.length > 0 && (activity.activityName === void 0 || typeof activity.activityName === "string") && isNonNegativeNumber(activity.totalXp) && isNonNegativeInteger(activity.masteredUnits) && isNonNegativeNumber(activity.activeTimeSeconds) && Number.isInteger(completionCount) && typeof completionCount === "number" && completionCount >= 0 && (activity.lastCompletedAt === void 0 || isIsoDateString(activity.lastCompletedAt));
3691
+ return typeof activity.runId === "string" && isValidUUID(activity.runId) && typeof activity.activityId === "string" && activity.activityId.length > 0 && (activity.activityName === void 0 || typeof activity.activityName === "string") && (activity.totalXp === void 0 || isNonNegativeNumber(activity.totalXp)) && (activity.masteredUnits === void 0 || isNonNegativeInteger(activity.masteredUnits)) && (activity.activeTimeSeconds === void 0 || isNonNegativeNumber(activity.activeTimeSeconds)) && (activity.score === void 0 || isNonNegativeNumber(activity.score) && activity.score <= 100);
3613
3692
  }
3614
3693
  function isValidGameMetricsResponse(value) {
3615
3694
  if (!value || typeof value !== "object") {
@@ -3626,9 +3705,15 @@ function isValidGameMetricsResponse(value) {
3626
3705
  }
3627
3706
  const metrics = course;
3628
3707
  const activities = metrics.activities;
3629
- return TIMEBACK_GRADES.has(metrics.grade) && TIMEBACK_SUBJECTS.has(metrics.subject) && isNonNegativeNumber(metrics.totalXp) && isNonNegativeInteger(metrics.masteredUnits) && isNonNegativeNumber(metrics.activeTimeSeconds) && (activities === void 0 || Array.isArray(activities) && activities.every(isValidActivity));
3708
+ return isValidGrade(metrics.grade) && isValidSubject(metrics.subject) && (metrics.totalXp === void 0 || isNonNegativeNumber(metrics.totalXp)) && (metrics.masteredUnits === void 0 || isNonNegativeInteger(metrics.masteredUnits)) && (metrics.activeTimeSeconds === void 0 || isNonNegativeNumber(metrics.activeTimeSeconds)) && (activities === void 0 || Array.isArray(activities) && activities.every(isValidRunMetrics));
3630
3709
  });
3631
3710
  }
3711
+ function getRequestedRunIds(request) {
3712
+ const runIds = new URL(request.url).searchParams.getAll("runId").filter((runId) => isValidUUID(runId));
3713
+ return [...new Set(runIds)];
3714
+ }
3715
+
3716
+ // ../edge-play/src/entry/metrics.ts
3632
3717
  function registerMetricsRoute(app, metricsModule) {
3633
3718
  app.get("/__playcademy/metrics", async (c) => {
3634
3719
  const claims = await verifyMetricsToken(c);
@@ -3645,6 +3730,7 @@ function registerMetricsRoute(app, metricsModule) {
3645
3730
  try {
3646
3731
  metrics = await metricsModule.getMetrics({
3647
3732
  studentId: claims.studentId,
3733
+ runIds: getRequestedRunIds(c.req.raw),
3648
3734
  env: c.env,
3649
3735
  sdk: c.get("sdk"),
3650
3736
  request: c.req.raw,
@@ -3746,8 +3832,8 @@ var cors = (options) => {
3746
3832
  };
3747
3833
 
3748
3834
  // ../sdk/src/core/guards.ts
3749
- var VALID_GRADES = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
3750
- var VALID_SUBJECTS = [
3835
+ var VALID_GRADES2 = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
3836
+ var VALID_SUBJECTS2 = [
3751
3837
  "Reading",
3752
3838
  "Language",
3753
3839
  "Vocabulary",
@@ -3758,11 +3844,11 @@ var VALID_SUBJECTS = [
3758
3844
  "Math",
3759
3845
  "None"
3760
3846
  ];
3761
- function isValidGrade(value) {
3762
- return typeof value === "number" && Number.isInteger(value) && VALID_GRADES.includes(value);
3847
+ function isValidGrade2(value) {
3848
+ return typeof value === "number" && Number.isInteger(value) && VALID_GRADES2.includes(value);
3763
3849
  }
3764
- function isValidSubject(value) {
3765
- return typeof value === "string" && VALID_SUBJECTS.includes(value);
3850
+ function isValidSubject2(value) {
3851
+ return typeof value === "string" && VALID_SUBJECTS2.includes(value);
3766
3852
  }
3767
3853
 
3768
3854
  // ../sdk/src/server/namespaces/timeback.ts
@@ -3820,10 +3906,10 @@ function createTimebackNamespace(client) {
3820
3906
  * ```
3821
3907
  */
3822
3908
  endActivity: async (studentId, payload) => {
3823
- if (!isValidGrade(payload.activityData.grade)) {
3909
+ if (!isValidGrade2(payload.activityData.grade)) {
3824
3910
  throw new Error("activityData.grade must be a valid grade level (-1 to 13)");
3825
3911
  }
3826
- if (!isValidSubject(payload.activityData.subject)) {
3912
+ if (!isValidSubject2(payload.activityData.subject)) {
3827
3913
  throw new Error("activityData.subject must be a valid subject");
3828
3914
  }
3829
3915
  const enrichedActivityData = enrichActivityData2(payload.activityData);
@@ -3879,14 +3965,14 @@ function createTimebackNamespace(client) {
3879
3965
  if (hasGrade !== hasSubject) {
3880
3966
  throw new Error("Both grade and subject must be provided together");
3881
3967
  }
3882
- if (hasGrade && !isValidGrade(options.grade)) {
3968
+ if (hasGrade && !isValidGrade2(options.grade)) {
3883
3969
  throw new Error(
3884
- `Invalid grade: ${options.grade}. Valid grades: ${VALID_GRADES.join(", ")}`
3970
+ `Invalid grade: ${options.grade}. Valid grades: ${VALID_GRADES2.join(", ")}`
3885
3971
  );
3886
3972
  }
3887
- if (hasSubject && !isValidSubject(options.subject)) {
3973
+ if (hasSubject && !isValidSubject2(options.subject)) {
3888
3974
  throw new Error(
3889
- `Invalid subject: ${options.subject}. Valid subjects: ${VALID_SUBJECTS.join(", ")}`
3975
+ `Invalid subject: ${options.subject}. Valid subjects: ${VALID_SUBJECTS2.join(", ")}`
3890
3976
  );
3891
3977
  }
3892
3978
  const params = new URLSearchParams();
@@ -3903,6 +3989,37 @@ function createTimebackNamespace(client) {
3903
3989
  const queryString = params.toString();
3904
3990
  const endpoint = `/api/timeback/student-xp/${studentId}?${queryString}`;
3905
3991
  return client["request"](endpoint, "GET");
3992
+ },
3993
+ getStudentMastery: async (studentId, options) => {
3994
+ const hasGrade = options?.grade !== void 0;
3995
+ const hasSubject = options?.subject !== void 0;
3996
+ if (hasGrade !== hasSubject) {
3997
+ throw new Error("Both grade and subject must be provided together");
3998
+ }
3999
+ if (hasGrade && !isValidGrade2(options.grade)) {
4000
+ throw new Error(
4001
+ `Invalid grade: ${options.grade}. Valid grades: ${VALID_GRADES2.join(", ")}`
4002
+ );
4003
+ }
4004
+ if (hasSubject && !isValidSubject2(options.subject)) {
4005
+ throw new Error(
4006
+ `Invalid subject: ${options.subject}. Valid subjects: ${VALID_SUBJECTS2.join(", ")}`
4007
+ );
4008
+ }
4009
+ const params = new URLSearchParams();
4010
+ params.set("gameId", client.gameId);
4011
+ if (options?.grade !== void 0) {
4012
+ params.set("grade", String(options.grade));
4013
+ }
4014
+ if (options?.subject) {
4015
+ params.set("subject", options.subject);
4016
+ }
4017
+ if (options?.include?.length) {
4018
+ params.set("include", options.include.join(","));
4019
+ }
4020
+ const queryString = params.toString();
4021
+ const endpoint = `/api/timeback/student-mastery/${studentId}?${queryString}`;
4022
+ return client["request"](endpoint, "GET");
3906
4023
  }
3907
4024
  };
3908
4025
  }
@@ -4499,14 +4616,16 @@ async function registerBuiltinRoutes(app, integrations) {
4499
4616
  const health = await Promise.resolve().then(() => (init_health(), health_exports));
4500
4617
  app.get(ROUTES.HEALTH, health.GET);
4501
4618
  if (integrations?.timeback) {
4502
- const [endActivity, getXp, heartbeat, advanceCourse] = await Promise.all([
4619
+ const [endActivity, getXp, getMastery, heartbeat, advanceCourse] = await Promise.all([
4503
4620
  Promise.resolve().then(() => (init_end_activity(), end_activity_exports)),
4504
4621
  Promise.resolve().then(() => (init_get_xp(), get_xp_exports)),
4622
+ Promise.resolve().then(() => (init_get_mastery(), get_mastery_exports)),
4505
4623
  Promise.resolve().then(() => (init_heartbeat(), heartbeat_exports)),
4506
4624
  Promise.resolve().then(() => (init_advance_course(), advance_course_exports))
4507
4625
  ]);
4508
4626
  app.post(ROUTES.TIMEBACK.END_ACTIVITY, endActivity.POST);
4509
4627
  app.get(ROUTES.TIMEBACK.GET_XP, getXp.GET);
4628
+ app.get(ROUTES.TIMEBACK.GET_MASTERY, getMastery.GET);
4510
4629
  app.post(ROUTES.TIMEBACK.HEARTBEAT, heartbeat.POST);
4511
4630
  app.post(ROUTES.TIMEBACK.ADVANCE_COURSE, advanceCourse.POST);
4512
4631
  } else if (integrations?.timeback === null) {
@@ -4525,6 +4644,14 @@ async function registerBuiltinRoutes(app, integrations) {
4525
4644
  __playcademyDevWarning: "timeback-not-configured"
4526
4645
  })
4527
4646
  );
4647
+ app.get(
4648
+ "/api/integrations/timeback/mastery",
4649
+ async (c) => c.json({
4650
+ totalMasteredUnits: 0,
4651
+ totalMasterableUnits: 0,
4652
+ __playcademyDevWarning: "timeback-not-configured"
4653
+ })
4654
+ );
4528
4655
  app.post(
4529
4656
  "/api/integrations/timeback/heartbeat",
4530
4657
  async (c) => c.json({
@@ -1,7 +1,7 @@
1
1
  {
2
- "cliVersion": "0.22.1",
3
- "sdkVersion": "0.10.0",
4
- "runtimeBuildId": "86c338e2c882",
5
- "inputFingerprint": "86c338e2c88222a4d95079601c19768a92c2bc085711d714d53c54b3aec80831",
2
+ "cliVersion": "0.22.2-beta.2",
3
+ "sdkVersion": "0.10.1-beta.2",
4
+ "runtimeBuildId": "9c6fe529d1f4",
5
+ "inputFingerprint": "9c6fe529d1f48ee9088ace05896b75194ea23409008e5aa76a5d927ab95a919d",
6
6
  "entry": "index.js"
7
7
  }
@@ -41,6 +41,7 @@ declare global {
41
41
 
42
42
  interface MetricsResolverContext {
43
43
  studentId: string
44
+ runIds: string[]
44
45
  env: PlaycademyEnv
45
46
  }
46
47
  }
package/dist/utils.js CHANGED
@@ -305,9 +305,21 @@ import { dirname as dirname3, resolve as resolve3 } from "path";
305
305
  var TIMEBACK_ROUTES = {
306
306
  END_ACTIVITY: "/integrations/timeback/end-activity",
307
307
  GET_XP: "/integrations/timeback/xp",
308
+ GET_MASTERY: "/integrations/timeback/mastery",
308
309
  HEARTBEAT: "/integrations/timeback/heartbeat",
309
310
  ADVANCE_COURSE: "/integrations/timeback/advance-course"
310
311
  };
312
+ var TIMEBACK_GAME_METRIC_DECIMAL_PLACES = {
313
+ xp: 1,
314
+ mastery: 0,
315
+ score: 2
316
+ };
317
+ var TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE = {
318
+ xp: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.xp,
319
+ mastery: 0,
320
+ time: 60,
321
+ score: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.score
322
+ };
311
323
 
312
324
  // ../constants/src/cloudflare.ts
313
325
  var WORKER_NAMING = {
@@ -345,7 +357,7 @@ var DEFAULT_API_ROUTES_DIRECTORY = join2(SERVER_ROOT_DIRECTORY, "api");
345
357
  // ../better-auth/package.json
346
358
  var package_default = {
347
359
  name: "@playcademy/better-auth",
348
- version: "0.0.14",
360
+ version: "0.0.15-beta.2",
349
361
  type: "module",
350
362
  exports: {
351
363
  "./server": {
@@ -1830,6 +1842,7 @@ var ROUTES = {
1830
1842
  TIMEBACK: {
1831
1843
  END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}`,
1832
1844
  GET_XP: `/api${TIMEBACK_ROUTES.GET_XP}`,
1845
+ GET_MASTERY: `/api${TIMEBACK_ROUTES.GET_MASTERY}`,
1833
1846
  HEARTBEAT: `/api${TIMEBACK_ROUTES.HEARTBEAT}`,
1834
1847
  ADVANCE_COURSE: `/api${TIMEBACK_ROUTES.ADVANCE_COURSE}`
1835
1848
  }
@@ -2455,7 +2468,7 @@ import { existsSync as existsSync9, mkdirSync as mkdirSync2, writeFileSync as wr
2455
2468
  import { dirname as dirname4, join as join14 } from "node:path";
2456
2469
 
2457
2470
  // src/version.ts
2458
- var cliVersion = false ? "0.0.0-dev" : "0.22.1";
2471
+ var cliVersion = false ? "0.0.0-dev" : "0.22.2-beta.2";
2459
2472
 
2460
2473
  // src/lib/build/binary-resource.ts
2461
2474
  function writeFileTree(baseDir, files) {
package/dist/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var cliVersion = false ? "0.0.0-dev" : "0.22.1";
2
+ var cliVersion = false ? "0.0.0-dev" : "0.22.2-beta.2";
3
3
  export {
4
4
  cliVersion
5
5
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playcademy",
3
- "version": "0.22.1",
3
+ "version": "0.22.2-beta.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {