@timeback/sdk 0.1.5 → 0.1.7

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.
Files changed (123) hide show
  1. package/README.md +21 -22
  2. package/dist/client/adapters/react/hooks/types.d.ts +15 -0
  3. package/dist/client/adapters/react/hooks/types.d.ts.map +1 -0
  4. package/dist/client/adapters/react/hooks/useTimebackVerification.d.ts +18 -0
  5. package/dist/client/adapters/react/hooks/useTimebackVerification.d.ts.map +1 -0
  6. package/dist/client/adapters/react/index.d.ts +2 -0
  7. package/dist/client/adapters/react/index.d.ts.map +1 -1
  8. package/dist/client/adapters/react/index.js +139 -9
  9. package/dist/client/auth/bearer.d.ts +17 -0
  10. package/dist/client/auth/bearer.d.ts.map +1 -0
  11. package/dist/client/auth/index.d.ts +3 -0
  12. package/dist/client/auth/index.d.ts.map +1 -0
  13. package/dist/client/auth/types.d.ts +39 -0
  14. package/dist/client/auth/types.d.ts.map +1 -0
  15. package/dist/client/index.d.ts +2 -0
  16. package/dist/client/index.d.ts.map +1 -1
  17. package/dist/client/lib/fetch.d.ts +19 -0
  18. package/dist/client/lib/fetch.d.ts.map +1 -0
  19. package/dist/client/namespaces/user.d.ts +25 -2
  20. package/dist/client/namespaces/user.d.ts.map +1 -1
  21. package/dist/client/timeback-client.class.d.ts +15 -0
  22. package/dist/client/timeback-client.class.d.ts.map +1 -1
  23. package/dist/client/timeback-client.d.ts +3 -0
  24. package/dist/client/timeback-client.d.ts.map +1 -1
  25. package/dist/client.d.ts +2 -1
  26. package/dist/client.d.ts.map +1 -1
  27. package/dist/client.js +69 -6
  28. package/dist/edge.d.ts +1 -1
  29. package/dist/edge.js +85291 -169
  30. package/dist/identity.js +85186 -74
  31. package/dist/index.js +1289 -840
  32. package/dist/server/adapters/express.d.ts.map +1 -1
  33. package/dist/server/adapters/express.js +489 -388
  34. package/dist/server/adapters/native.d.ts.map +1 -1
  35. package/dist/server/adapters/native.js +32 -1
  36. package/dist/server/adapters/nextjs.js +32 -1
  37. package/dist/server/adapters/nuxt.d.ts.map +1 -1
  38. package/dist/server/adapters/nuxt.js +493 -388
  39. package/dist/server/adapters/solid-start.d.ts.map +1 -1
  40. package/dist/server/adapters/solid-start.js +493 -388
  41. package/dist/server/adapters/svelte-kit.d.ts.map +1 -1
  42. package/dist/server/adapters/svelte-kit.js +37 -1
  43. package/dist/server/adapters/tanstack-start.d.ts.map +1 -1
  44. package/dist/server/adapters/tanstack-start.js +488 -388
  45. package/dist/server/adapters/utils.d.ts +1 -1
  46. package/dist/server/adapters/utils.d.ts.map +1 -1
  47. package/dist/server/{lib/build-activity-events.d.ts → handlers/activity/caliper.d.ts} +29 -4
  48. package/dist/server/handlers/activity/caliper.d.ts.map +1 -0
  49. package/dist/server/handlers/activity/gradebook.d.ts +56 -0
  50. package/dist/server/handlers/activity/gradebook.d.ts.map +1 -0
  51. package/dist/server/handlers/activity/handler.d.ts +15 -0
  52. package/dist/server/handlers/activity/handler.d.ts.map +1 -0
  53. package/dist/server/handlers/activity/index.d.ts +9 -0
  54. package/dist/server/handlers/activity/index.d.ts.map +1 -0
  55. package/dist/server/handlers/activity/resolve.d.ts +39 -0
  56. package/dist/server/handlers/activity/resolve.d.ts.map +1 -0
  57. package/dist/server/handlers/activity/schema.d.ts +52 -0
  58. package/dist/server/handlers/activity/schema.d.ts.map +1 -0
  59. package/dist/server/handlers/activity/types.d.ts +52 -0
  60. package/dist/server/handlers/activity/types.d.ts.map +1 -0
  61. package/dist/server/handlers/identity/handler.d.ts +14 -0
  62. package/dist/server/handlers/identity/handler.d.ts.map +1 -0
  63. package/dist/server/handlers/identity/index.d.ts +8 -0
  64. package/dist/server/handlers/identity/index.d.ts.map +1 -0
  65. package/dist/server/handlers/identity/oidc.d.ts +43 -0
  66. package/dist/server/handlers/identity/oidc.d.ts.map +1 -0
  67. package/dist/server/handlers/identity/types.d.ts +24 -0
  68. package/dist/server/handlers/identity/types.d.ts.map +1 -0
  69. package/dist/server/handlers/identity-only/handler.d.ts +15 -0
  70. package/dist/server/handlers/identity-only/handler.d.ts.map +1 -0
  71. package/dist/server/handlers/identity-only/index.d.ts +8 -0
  72. package/dist/server/handlers/identity-only/index.d.ts.map +1 -0
  73. package/dist/server/handlers/identity-only/oidc.d.ts +26 -0
  74. package/dist/server/handlers/identity-only/oidc.d.ts.map +1 -0
  75. package/dist/server/handlers/identity-only/types.d.ts +19 -0
  76. package/dist/server/handlers/identity-only/types.d.ts.map +1 -0
  77. package/dist/server/handlers/index.d.ts +5 -2
  78. package/dist/server/handlers/index.d.ts.map +1 -1
  79. package/dist/server/{lib/build-user-profile.d.ts → handlers/user/enrollments.d.ts} +7 -2
  80. package/dist/server/handlers/user/enrollments.d.ts.map +1 -0
  81. package/dist/server/handlers/user/handler.d.ts +17 -0
  82. package/dist/server/handlers/user/handler.d.ts.map +1 -0
  83. package/dist/server/handlers/user/index.d.ts +10 -0
  84. package/dist/server/handlers/user/index.d.ts.map +1 -0
  85. package/dist/server/handlers/user/profile.d.ts +22 -0
  86. package/dist/server/handlers/user/profile.d.ts.map +1 -0
  87. package/dist/server/handlers/user/types.d.ts +35 -0
  88. package/dist/server/handlers/user/types.d.ts.map +1 -0
  89. package/dist/server/handlers/user/verify.d.ts +25 -0
  90. package/dist/server/handlers/user/verify.d.ts.map +1 -0
  91. package/dist/server/index.d.ts +1 -1
  92. package/dist/server/index.d.ts.map +1 -1
  93. package/dist/server/lib/index.d.ts +4 -5
  94. package/dist/server/lib/index.d.ts.map +1 -1
  95. package/dist/server/lib/resolve.d.ts +4 -42
  96. package/dist/server/lib/resolve.d.ts.map +1 -1
  97. package/dist/server/lib/sso.d.ts +86 -0
  98. package/dist/server/lib/sso.d.ts.map +1 -0
  99. package/dist/server/lib/utils.d.ts +32 -1
  100. package/dist/server/lib/utils.d.ts.map +1 -1
  101. package/dist/server/timeback-identity.d.ts +2 -2
  102. package/dist/server/timeback-identity.d.ts.map +1 -1
  103. package/dist/server/timeback.d.ts.map +1 -1
  104. package/dist/server/types.d.ts +19 -12
  105. package/dist/server/types.d.ts.map +1 -1
  106. package/dist/shared/constants.d.ts +1 -0
  107. package/dist/shared/constants.d.ts.map +1 -1
  108. package/dist/shared/types.d.ts +18 -3
  109. package/dist/shared/types.d.ts.map +1 -1
  110. package/package.json +7 -7
  111. package/dist/config.d.ts +0 -20
  112. package/dist/config.d.ts.map +0 -1
  113. package/dist/config.js +0 -0
  114. package/dist/server/handlers/activity.d.ts +0 -25
  115. package/dist/server/handlers/activity.d.ts.map +0 -1
  116. package/dist/server/handlers/identity-full.d.ts +0 -28
  117. package/dist/server/handlers/identity-full.d.ts.map +0 -1
  118. package/dist/server/handlers/identity-only.d.ts +0 -22
  119. package/dist/server/handlers/identity-only.d.ts.map +0 -1
  120. package/dist/server/handlers/user.d.ts +0 -31
  121. package/dist/server/handlers/user.d.ts.map +0 -1
  122. package/dist/server/lib/build-activity-events.d.ts.map +0 -1
  123. package/dist/server/lib/build-user-profile.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -14949,7 +14949,7 @@ var TimebackSubject = exports_external.enum([
14949
14949
  "Math",
14950
14950
  "None",
14951
14951
  "Other"
14952
- ]);
14952
+ ]).meta({ id: "TimebackSubject", description: "Subject area" });
14953
14953
  var TimebackGrade = exports_external.union([
14954
14954
  exports_external.literal(-1),
14955
14955
  exports_external.literal(0),
@@ -14966,7 +14966,10 @@ var TimebackGrade = exports_external.union([
14966
14966
  exports_external.literal(11),
14967
14967
  exports_external.literal(12),
14968
14968
  exports_external.literal(13)
14969
- ]);
14969
+ ]).meta({
14970
+ id: "TimebackGrade",
14971
+ description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
14972
+ });
14970
14973
  var ScoreStatus = exports_external.enum([
14971
14974
  "exempt",
14972
14975
  "fully graded",
@@ -15196,62 +15199,84 @@ var CaliperListEventsParams = exports_external.object({
15196
15199
  actorEmail: exports_external.email().optional()
15197
15200
  }).strict();
15198
15201
  var CourseIds = exports_external.object({
15199
- staging: exports_external.string().optional(),
15200
- production: exports_external.string().optional()
15201
- });
15202
- var CourseType = exports_external.enum(["base", "hole-filling", "optional"]);
15203
- var PublishStatus = exports_external.enum(["draft", "testing", "published", "deactivated"]);
15202
+ staging: exports_external.string().meta({ description: "Course ID in staging environment" }).optional(),
15203
+ production: exports_external.string().meta({ description: "Course ID in production environment" }).optional()
15204
+ }).meta({ id: "CourseIds", description: "Environment-specific course IDs (populated by sync)" });
15205
+ var CourseType = exports_external.enum(["base", "hole-filling", "optional"]).meta({ id: "CourseType", description: "Course classification type" });
15206
+ var PublishStatus = exports_external.enum(["draft", "testing", "published", "deactivated"]).meta({ id: "PublishStatus", description: "Course publication status" });
15204
15207
  var CourseGoals = exports_external.object({
15205
- dailyXp: exports_external.number().int().positive().optional(),
15206
- dailyLessons: exports_external.number().int().positive().optional(),
15207
- dailyActiveMinutes: exports_external.number().int().positive().optional(),
15208
- dailyAccuracy: exports_external.number().int().min(0).max(100).optional(),
15209
- dailyMasteredUnits: exports_external.number().int().positive().optional()
15210
- });
15208
+ dailyXp: exports_external.number().int().positive().meta({ description: "Target XP to earn per day" }).optional(),
15209
+ dailyLessons: exports_external.number().int().positive().meta({ description: "Target lessons to complete per day" }).optional(),
15210
+ dailyActiveMinutes: exports_external.number().int().positive().meta({ description: "Target active learning minutes per day" }).optional(),
15211
+ dailyAccuracy: exports_external.number().int().min(0).max(100).meta({ description: "Target accuracy percentage (0-100)" }).optional(),
15212
+ dailyMasteredUnits: exports_external.number().int().positive().meta({ description: "Target units to master per day" }).optional()
15213
+ }).meta({ id: "CourseGoals", description: "Daily learning goals for a course" });
15211
15214
  var CourseMetrics = exports_external.object({
15212
- totalXp: exports_external.number().int().positive().optional(),
15213
- totalLessons: exports_external.number().int().positive().optional(),
15214
- totalGrades: exports_external.number().int().positive().optional()
15215
- });
15215
+ totalXp: exports_external.number().int().positive().meta({ description: "Total XP available in the course" }).optional(),
15216
+ totalLessons: exports_external.number().int().positive().meta({ description: "Total number of lessons/activities" }).optional(),
15217
+ totalGrades: exports_external.number().int().positive().meta({ description: "Total grade levels covered" }).optional()
15218
+ }).meta({ id: "CourseMetrics", description: "Aggregate metrics for a course" });
15216
15219
  var CourseMetadata = exports_external.object({
15217
15220
  courseType: CourseType.optional(),
15218
- isSupplemental: exports_external.boolean().optional(),
15219
- isCustom: exports_external.boolean().optional(),
15221
+ isSupplemental: exports_external.boolean().meta({ description: "Whether this is supplemental to a base course" }).optional(),
15222
+ isCustom: exports_external.boolean().meta({ description: "Whether this is a custom course for an individual student" }).optional(),
15220
15223
  publishStatus: PublishStatus.optional(),
15221
- contactEmail: exports_external.email().optional(),
15222
- primaryApp: exports_external.string().optional(),
15224
+ contactEmail: exports_external.email().meta({ description: "Contact email for course issues" }).optional(),
15225
+ primaryApp: exports_external.string().meta({ description: "Primary application identifier" }).optional(),
15223
15226
  goals: CourseGoals.optional(),
15224
15227
  metrics: CourseMetrics.optional()
15225
- });
15228
+ }).meta({ id: "CourseMetadata", description: "Course metadata (matches API metadata object)" });
15226
15229
  var CourseDefaults = exports_external.object({
15227
- courseCode: exports_external.string().optional(),
15228
- level: exports_external.string().optional(),
15230
+ courseCode: exports_external.string().meta({ description: "Course code (e.g., 'MATH101')" }).optional(),
15231
+ level: exports_external.string().meta({ description: "Course level (e.g., 'AP', 'Honors')" }).optional(),
15229
15232
  metadata: CourseMetadata.optional()
15233
+ }).meta({
15234
+ id: "CourseDefaults",
15235
+ description: "Default properties that apply to all courses unless overridden"
15230
15236
  });
15231
15237
  var CourseEnvOverrides = exports_external.object({
15232
- level: exports_external.string().optional(),
15233
- sensor: exports_external.string().url().optional(),
15234
- launchUrl: exports_external.string().url().optional(),
15238
+ level: exports_external.string().meta({ description: "Course level for this environment" }).optional(),
15239
+ sensor: exports_external.url().meta({ description: "Caliper sensor endpoint URL for this environment" }).optional(),
15240
+ launchUrl: exports_external.url().meta({ description: "LTI launch URL for this environment" }).optional(),
15235
15241
  metadata: CourseMetadata.optional()
15242
+ }).meta({
15243
+ id: "CourseEnvOverrides",
15244
+ description: "Environment-specific course overrides (non-identity fields)"
15236
15245
  });
15237
15246
  var CourseOverrides = exports_external.object({
15238
- staging: CourseEnvOverrides.optional(),
15239
- production: CourseEnvOverrides.optional()
15240
- });
15247
+ staging: CourseEnvOverrides.meta({
15248
+ description: "Overrides for staging environment"
15249
+ }).optional(),
15250
+ production: CourseEnvOverrides.meta({
15251
+ description: "Overrides for production environment"
15252
+ }).optional()
15253
+ }).meta({ id: "CourseOverrides", description: "Per-environment course overrides" });
15241
15254
  var CourseConfig = CourseDefaults.extend({
15242
- subject: TimebackSubject,
15243
- grade: TimebackGrade.optional(),
15255
+ subject: TimebackSubject.meta({ description: "Subject area for this course" }),
15256
+ grade: TimebackGrade.meta({
15257
+ description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
15258
+ }).optional(),
15244
15259
  ids: CourseIds.nullable().optional(),
15245
- sensor: exports_external.string().url().optional(),
15246
- launchUrl: exports_external.string().url().optional(),
15260
+ sensor: exports_external.url().meta({ description: "Caliper sensor endpoint URL for this course" }).optional(),
15261
+ launchUrl: exports_external.url().meta({ description: "LTI launch URL for this course" }).optional(),
15247
15262
  overrides: CourseOverrides.optional()
15263
+ }).meta({
15264
+ id: "CourseConfig",
15265
+ description: "Configuration for a single course. Must have either grade or courseCode (or both)."
15248
15266
  });
15249
15267
  var TimebackConfig = exports_external.object({
15250
- name: exports_external.string().min(1, "App name is required"),
15251
- defaults: CourseDefaults.optional(),
15252
- courses: exports_external.array(CourseConfig).min(1, "At least one course is required"),
15253
- sensor: exports_external.string().url().optional(),
15254
- launchUrl: exports_external.string().url().optional()
15268
+ $schema: exports_external.string().meta({ description: "JSON Schema reference for editor support" }).optional(),
15269
+ name: exports_external.string().min(1, "App name is required").meta({ description: "Display name for your app" }),
15270
+ defaults: CourseDefaults.meta({
15271
+ description: "Default properties applied to all courses"
15272
+ }).optional(),
15273
+ courses: exports_external.array(CourseConfig).min(1, "At least one course is required").meta({ description: "Courses available in this app" }),
15274
+ sensor: exports_external.url().meta({ description: "Default Caliper sensor endpoint URL for all courses" }).optional(),
15275
+ launchUrl: exports_external.url().meta({ description: "Default LTI launch URL for all courses" }).optional()
15276
+ }).meta({
15277
+ id: "TimebackConfig",
15278
+ title: "Timeback Config",
15279
+ description: "Configuration schema for timeback.config.json files"
15255
15280
  }).refine((config2) => {
15256
15281
  return config2.courses.every((c) => c.grade !== undefined || c.courseCode !== undefined);
15257
15282
  }, {
@@ -15272,14 +15297,20 @@ var TimebackConfig = exports_external.object({
15272
15297
  message: "Duplicate courseCode found; each must be unique",
15273
15298
  path: ["courses"]
15274
15299
  }).refine((config2) => {
15275
- return config2.courses.every((c) => c.sensor !== undefined || config2.sensor !== undefined);
15276
- }, {
15277
- message: "Each course must have an effective sensor; set a top-level `sensor` or per-course `sensor`",
15278
- path: ["courses"]
15279
- }).refine((config2) => {
15280
- return config2.courses.every((c) => c.launchUrl !== undefined || config2.launchUrl !== undefined);
15300
+ return config2.courses.every((c) => {
15301
+ if (c.sensor !== undefined || config2.sensor !== undefined) {
15302
+ return true;
15303
+ }
15304
+ const launchUrls = [
15305
+ c.launchUrl,
15306
+ config2.launchUrl,
15307
+ c.overrides?.staging?.launchUrl,
15308
+ c.overrides?.production?.launchUrl
15309
+ ].filter(Boolean);
15310
+ return launchUrls.length > 0;
15311
+ });
15281
15312
  }, {
15282
- message: "Each course must have an effective launchUrl; set a top-level `launchUrl` or per-course `launchUrl`",
15313
+ message: "Each course must have an effective sensor. Either set `sensor` explicitly (top-level or per-course), or provide a `launchUrl` so sensor can be derived from its origin.",
15283
15314
  path: ["courses"]
15284
15315
  });
15285
15316
  var EdubridgeDateString = exports_external.union([IsoDateString, IsoDateTimeString]);
@@ -31327,7 +31358,7 @@ var TimebackSubject2 = exports_external2.enum([
31327
31358
  "Math",
31328
31359
  "None",
31329
31360
  "Other"
31330
- ]);
31361
+ ]).meta({ id: "TimebackSubject", description: "Subject area" });
31331
31362
  var TimebackGrade2 = exports_external2.union([
31332
31363
  exports_external2.literal(-1),
31333
31364
  exports_external2.literal(0),
@@ -31344,7 +31375,10 @@ var TimebackGrade2 = exports_external2.union([
31344
31375
  exports_external2.literal(11),
31345
31376
  exports_external2.literal(12),
31346
31377
  exports_external2.literal(13)
31347
- ]);
31378
+ ]).meta({
31379
+ id: "TimebackGrade",
31380
+ description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
31381
+ });
31348
31382
  var ScoreStatus2 = exports_external2.enum([
31349
31383
  "exempt",
31350
31384
  "fully graded",
@@ -31574,62 +31608,84 @@ var CaliperListEventsParams2 = exports_external2.object({
31574
31608
  actorEmail: exports_external2.email().optional()
31575
31609
  }).strict();
31576
31610
  var CourseIds2 = exports_external2.object({
31577
- staging: exports_external2.string().optional(),
31578
- production: exports_external2.string().optional()
31579
- });
31580
- var CourseType2 = exports_external2.enum(["base", "hole-filling", "optional"]);
31581
- var PublishStatus2 = exports_external2.enum(["draft", "testing", "published", "deactivated"]);
31611
+ staging: exports_external2.string().meta({ description: "Course ID in staging environment" }).optional(),
31612
+ production: exports_external2.string().meta({ description: "Course ID in production environment" }).optional()
31613
+ }).meta({ id: "CourseIds", description: "Environment-specific course IDs (populated by sync)" });
31614
+ var CourseType2 = exports_external2.enum(["base", "hole-filling", "optional"]).meta({ id: "CourseType", description: "Course classification type" });
31615
+ var PublishStatus2 = exports_external2.enum(["draft", "testing", "published", "deactivated"]).meta({ id: "PublishStatus", description: "Course publication status" });
31582
31616
  var CourseGoals2 = exports_external2.object({
31583
- dailyXp: exports_external2.number().int().positive().optional(),
31584
- dailyLessons: exports_external2.number().int().positive().optional(),
31585
- dailyActiveMinutes: exports_external2.number().int().positive().optional(),
31586
- dailyAccuracy: exports_external2.number().int().min(0).max(100).optional(),
31587
- dailyMasteredUnits: exports_external2.number().int().positive().optional()
31588
- });
31617
+ dailyXp: exports_external2.number().int().positive().meta({ description: "Target XP to earn per day" }).optional(),
31618
+ dailyLessons: exports_external2.number().int().positive().meta({ description: "Target lessons to complete per day" }).optional(),
31619
+ dailyActiveMinutes: exports_external2.number().int().positive().meta({ description: "Target active learning minutes per day" }).optional(),
31620
+ dailyAccuracy: exports_external2.number().int().min(0).max(100).meta({ description: "Target accuracy percentage (0-100)" }).optional(),
31621
+ dailyMasteredUnits: exports_external2.number().int().positive().meta({ description: "Target units to master per day" }).optional()
31622
+ }).meta({ id: "CourseGoals", description: "Daily learning goals for a course" });
31589
31623
  var CourseMetrics2 = exports_external2.object({
31590
- totalXp: exports_external2.number().int().positive().optional(),
31591
- totalLessons: exports_external2.number().int().positive().optional(),
31592
- totalGrades: exports_external2.number().int().positive().optional()
31593
- });
31624
+ totalXp: exports_external2.number().int().positive().meta({ description: "Total XP available in the course" }).optional(),
31625
+ totalLessons: exports_external2.number().int().positive().meta({ description: "Total number of lessons/activities" }).optional(),
31626
+ totalGrades: exports_external2.number().int().positive().meta({ description: "Total grade levels covered" }).optional()
31627
+ }).meta({ id: "CourseMetrics", description: "Aggregate metrics for a course" });
31594
31628
  var CourseMetadata2 = exports_external2.object({
31595
31629
  courseType: CourseType2.optional(),
31596
- isSupplemental: exports_external2.boolean().optional(),
31597
- isCustom: exports_external2.boolean().optional(),
31630
+ isSupplemental: exports_external2.boolean().meta({ description: "Whether this is supplemental to a base course" }).optional(),
31631
+ isCustom: exports_external2.boolean().meta({ description: "Whether this is a custom course for an individual student" }).optional(),
31598
31632
  publishStatus: PublishStatus2.optional(),
31599
- contactEmail: exports_external2.email().optional(),
31600
- primaryApp: exports_external2.string().optional(),
31633
+ contactEmail: exports_external2.email().meta({ description: "Contact email for course issues" }).optional(),
31634
+ primaryApp: exports_external2.string().meta({ description: "Primary application identifier" }).optional(),
31601
31635
  goals: CourseGoals2.optional(),
31602
31636
  metrics: CourseMetrics2.optional()
31603
- });
31637
+ }).meta({ id: "CourseMetadata", description: "Course metadata (matches API metadata object)" });
31604
31638
  var CourseDefaults2 = exports_external2.object({
31605
- courseCode: exports_external2.string().optional(),
31606
- level: exports_external2.string().optional(),
31639
+ courseCode: exports_external2.string().meta({ description: "Course code (e.g., 'MATH101')" }).optional(),
31640
+ level: exports_external2.string().meta({ description: "Course level (e.g., 'AP', 'Honors')" }).optional(),
31607
31641
  metadata: CourseMetadata2.optional()
31642
+ }).meta({
31643
+ id: "CourseDefaults",
31644
+ description: "Default properties that apply to all courses unless overridden"
31608
31645
  });
31609
31646
  var CourseEnvOverrides2 = exports_external2.object({
31610
- level: exports_external2.string().optional(),
31611
- sensor: exports_external2.string().url().optional(),
31612
- launchUrl: exports_external2.string().url().optional(),
31647
+ level: exports_external2.string().meta({ description: "Course level for this environment" }).optional(),
31648
+ sensor: exports_external2.url().meta({ description: "Caliper sensor endpoint URL for this environment" }).optional(),
31649
+ launchUrl: exports_external2.url().meta({ description: "LTI launch URL for this environment" }).optional(),
31613
31650
  metadata: CourseMetadata2.optional()
31651
+ }).meta({
31652
+ id: "CourseEnvOverrides",
31653
+ description: "Environment-specific course overrides (non-identity fields)"
31614
31654
  });
31615
31655
  var CourseOverrides2 = exports_external2.object({
31616
- staging: CourseEnvOverrides2.optional(),
31617
- production: CourseEnvOverrides2.optional()
31618
- });
31656
+ staging: CourseEnvOverrides2.meta({
31657
+ description: "Overrides for staging environment"
31658
+ }).optional(),
31659
+ production: CourseEnvOverrides2.meta({
31660
+ description: "Overrides for production environment"
31661
+ }).optional()
31662
+ }).meta({ id: "CourseOverrides", description: "Per-environment course overrides" });
31619
31663
  var CourseConfig2 = CourseDefaults2.extend({
31620
- subject: TimebackSubject2,
31621
- grade: TimebackGrade2.optional(),
31664
+ subject: TimebackSubject2.meta({ description: "Subject area for this course" }),
31665
+ grade: TimebackGrade2.meta({
31666
+ description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
31667
+ }).optional(),
31622
31668
  ids: CourseIds2.nullable().optional(),
31623
- sensor: exports_external2.string().url().optional(),
31624
- launchUrl: exports_external2.string().url().optional(),
31669
+ sensor: exports_external2.url().meta({ description: "Caliper sensor endpoint URL for this course" }).optional(),
31670
+ launchUrl: exports_external2.url().meta({ description: "LTI launch URL for this course" }).optional(),
31625
31671
  overrides: CourseOverrides2.optional()
31672
+ }).meta({
31673
+ id: "CourseConfig",
31674
+ description: "Configuration for a single course. Must have either grade or courseCode (or both)."
31626
31675
  });
31627
31676
  var TimebackConfig2 = exports_external2.object({
31628
- name: exports_external2.string().min(1, "App name is required"),
31629
- defaults: CourseDefaults2.optional(),
31630
- courses: exports_external2.array(CourseConfig2).min(1, "At least one course is required"),
31631
- sensor: exports_external2.string().url().optional(),
31632
- launchUrl: exports_external2.string().url().optional()
31677
+ $schema: exports_external2.string().meta({ description: "JSON Schema reference for editor support" }).optional(),
31678
+ name: exports_external2.string().min(1, "App name is required").meta({ description: "Display name for your app" }),
31679
+ defaults: CourseDefaults2.meta({
31680
+ description: "Default properties applied to all courses"
31681
+ }).optional(),
31682
+ courses: exports_external2.array(CourseConfig2).min(1, "At least one course is required").meta({ description: "Courses available in this app" }),
31683
+ sensor: exports_external2.url().meta({ description: "Default Caliper sensor endpoint URL for all courses" }).optional(),
31684
+ launchUrl: exports_external2.url().meta({ description: "Default LTI launch URL for all courses" }).optional()
31685
+ }).meta({
31686
+ id: "TimebackConfig",
31687
+ title: "Timeback Config",
31688
+ description: "Configuration schema for timeback.config.json files"
31633
31689
  }).refine((config22) => {
31634
31690
  return config22.courses.every((c) => c.grade !== undefined || c.courseCode !== undefined);
31635
31691
  }, {
@@ -31650,14 +31706,20 @@ var TimebackConfig2 = exports_external2.object({
31650
31706
  message: "Duplicate courseCode found; each must be unique",
31651
31707
  path: ["courses"]
31652
31708
  }).refine((config22) => {
31653
- return config22.courses.every((c) => c.sensor !== undefined || config22.sensor !== undefined);
31654
- }, {
31655
- message: "Each course must have an effective sensor; set a top-level `sensor` or per-course `sensor`",
31656
- path: ["courses"]
31657
- }).refine((config22) => {
31658
- return config22.courses.every((c) => c.launchUrl !== undefined || config22.launchUrl !== undefined);
31709
+ return config22.courses.every((c) => {
31710
+ if (c.sensor !== undefined || config22.sensor !== undefined) {
31711
+ return true;
31712
+ }
31713
+ const launchUrls = [
31714
+ c.launchUrl,
31715
+ config22.launchUrl,
31716
+ c.overrides?.staging?.launchUrl,
31717
+ c.overrides?.production?.launchUrl
31718
+ ].filter(Boolean);
31719
+ return launchUrls.length > 0;
31720
+ });
31659
31721
  }, {
31660
- message: "Each course must have an effective launchUrl; set a top-level `launchUrl` or per-course `launchUrl`",
31722
+ message: "Each course must have an effective sensor. Either set `sensor` explicitly (top-level or per-course), or provide a `launchUrl` so sensor can be derived from its origin.",
31661
31723
  path: ["courses"]
31662
31724
  });
31663
31725
  var EdubridgeDateString2 = exports_external2.union([IsoDateString2, IsoDateTimeString2]);
@@ -48735,7 +48797,7 @@ var TimebackSubject3 = exports_external3.enum([
48735
48797
  "Math",
48736
48798
  "None",
48737
48799
  "Other"
48738
- ]);
48800
+ ]).meta({ id: "TimebackSubject", description: "Subject area" });
48739
48801
  var TimebackGrade3 = exports_external3.union([
48740
48802
  exports_external3.literal(-1),
48741
48803
  exports_external3.literal(0),
@@ -48752,7 +48814,10 @@ var TimebackGrade3 = exports_external3.union([
48752
48814
  exports_external3.literal(11),
48753
48815
  exports_external3.literal(12),
48754
48816
  exports_external3.literal(13)
48755
- ]);
48817
+ ]).meta({
48818
+ id: "TimebackGrade",
48819
+ description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
48820
+ });
48756
48821
  var ScoreStatus3 = exports_external3.enum([
48757
48822
  "exempt",
48758
48823
  "fully graded",
@@ -48982,62 +49047,84 @@ var CaliperListEventsParams3 = exports_external3.object({
48982
49047
  actorEmail: exports_external3.email().optional()
48983
49048
  }).strict();
48984
49049
  var CourseIds3 = exports_external3.object({
48985
- staging: exports_external3.string().optional(),
48986
- production: exports_external3.string().optional()
48987
- });
48988
- var CourseType3 = exports_external3.enum(["base", "hole-filling", "optional"]);
48989
- var PublishStatus3 = exports_external3.enum(["draft", "testing", "published", "deactivated"]);
49050
+ staging: exports_external3.string().meta({ description: "Course ID in staging environment" }).optional(),
49051
+ production: exports_external3.string().meta({ description: "Course ID in production environment" }).optional()
49052
+ }).meta({ id: "CourseIds", description: "Environment-specific course IDs (populated by sync)" });
49053
+ var CourseType3 = exports_external3.enum(["base", "hole-filling", "optional"]).meta({ id: "CourseType", description: "Course classification type" });
49054
+ var PublishStatus3 = exports_external3.enum(["draft", "testing", "published", "deactivated"]).meta({ id: "PublishStatus", description: "Course publication status" });
48990
49055
  var CourseGoals3 = exports_external3.object({
48991
- dailyXp: exports_external3.number().int().positive().optional(),
48992
- dailyLessons: exports_external3.number().int().positive().optional(),
48993
- dailyActiveMinutes: exports_external3.number().int().positive().optional(),
48994
- dailyAccuracy: exports_external3.number().int().min(0).max(100).optional(),
48995
- dailyMasteredUnits: exports_external3.number().int().positive().optional()
48996
- });
49056
+ dailyXp: exports_external3.number().int().positive().meta({ description: "Target XP to earn per day" }).optional(),
49057
+ dailyLessons: exports_external3.number().int().positive().meta({ description: "Target lessons to complete per day" }).optional(),
49058
+ dailyActiveMinutes: exports_external3.number().int().positive().meta({ description: "Target active learning minutes per day" }).optional(),
49059
+ dailyAccuracy: exports_external3.number().int().min(0).max(100).meta({ description: "Target accuracy percentage (0-100)" }).optional(),
49060
+ dailyMasteredUnits: exports_external3.number().int().positive().meta({ description: "Target units to master per day" }).optional()
49061
+ }).meta({ id: "CourseGoals", description: "Daily learning goals for a course" });
48997
49062
  var CourseMetrics3 = exports_external3.object({
48998
- totalXp: exports_external3.number().int().positive().optional(),
48999
- totalLessons: exports_external3.number().int().positive().optional(),
49000
- totalGrades: exports_external3.number().int().positive().optional()
49001
- });
49063
+ totalXp: exports_external3.number().int().positive().meta({ description: "Total XP available in the course" }).optional(),
49064
+ totalLessons: exports_external3.number().int().positive().meta({ description: "Total number of lessons/activities" }).optional(),
49065
+ totalGrades: exports_external3.number().int().positive().meta({ description: "Total grade levels covered" }).optional()
49066
+ }).meta({ id: "CourseMetrics", description: "Aggregate metrics for a course" });
49002
49067
  var CourseMetadata3 = exports_external3.object({
49003
49068
  courseType: CourseType3.optional(),
49004
- isSupplemental: exports_external3.boolean().optional(),
49005
- isCustom: exports_external3.boolean().optional(),
49069
+ isSupplemental: exports_external3.boolean().meta({ description: "Whether this is supplemental to a base course" }).optional(),
49070
+ isCustom: exports_external3.boolean().meta({ description: "Whether this is a custom course for an individual student" }).optional(),
49006
49071
  publishStatus: PublishStatus3.optional(),
49007
- contactEmail: exports_external3.email().optional(),
49008
- primaryApp: exports_external3.string().optional(),
49072
+ contactEmail: exports_external3.email().meta({ description: "Contact email for course issues" }).optional(),
49073
+ primaryApp: exports_external3.string().meta({ description: "Primary application identifier" }).optional(),
49009
49074
  goals: CourseGoals3.optional(),
49010
49075
  metrics: CourseMetrics3.optional()
49011
- });
49076
+ }).meta({ id: "CourseMetadata", description: "Course metadata (matches API metadata object)" });
49012
49077
  var CourseDefaults3 = exports_external3.object({
49013
- courseCode: exports_external3.string().optional(),
49014
- level: exports_external3.string().optional(),
49078
+ courseCode: exports_external3.string().meta({ description: "Course code (e.g., 'MATH101')" }).optional(),
49079
+ level: exports_external3.string().meta({ description: "Course level (e.g., 'AP', 'Honors')" }).optional(),
49015
49080
  metadata: CourseMetadata3.optional()
49081
+ }).meta({
49082
+ id: "CourseDefaults",
49083
+ description: "Default properties that apply to all courses unless overridden"
49016
49084
  });
49017
49085
  var CourseEnvOverrides3 = exports_external3.object({
49018
- level: exports_external3.string().optional(),
49019
- sensor: exports_external3.string().url().optional(),
49020
- launchUrl: exports_external3.string().url().optional(),
49086
+ level: exports_external3.string().meta({ description: "Course level for this environment" }).optional(),
49087
+ sensor: exports_external3.url().meta({ description: "Caliper sensor endpoint URL for this environment" }).optional(),
49088
+ launchUrl: exports_external3.url().meta({ description: "LTI launch URL for this environment" }).optional(),
49021
49089
  metadata: CourseMetadata3.optional()
49090
+ }).meta({
49091
+ id: "CourseEnvOverrides",
49092
+ description: "Environment-specific course overrides (non-identity fields)"
49022
49093
  });
49023
49094
  var CourseOverrides3 = exports_external3.object({
49024
- staging: CourseEnvOverrides3.optional(),
49025
- production: CourseEnvOverrides3.optional()
49026
- });
49095
+ staging: CourseEnvOverrides3.meta({
49096
+ description: "Overrides for staging environment"
49097
+ }).optional(),
49098
+ production: CourseEnvOverrides3.meta({
49099
+ description: "Overrides for production environment"
49100
+ }).optional()
49101
+ }).meta({ id: "CourseOverrides", description: "Per-environment course overrides" });
49027
49102
  var CourseConfig3 = CourseDefaults3.extend({
49028
- subject: TimebackSubject3,
49029
- grade: TimebackGrade3.optional(),
49103
+ subject: TimebackSubject3.meta({ description: "Subject area for this course" }),
49104
+ grade: TimebackGrade3.meta({
49105
+ description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
49106
+ }).optional(),
49030
49107
  ids: CourseIds3.nullable().optional(),
49031
- sensor: exports_external3.string().url().optional(),
49032
- launchUrl: exports_external3.string().url().optional(),
49108
+ sensor: exports_external3.url().meta({ description: "Caliper sensor endpoint URL for this course" }).optional(),
49109
+ launchUrl: exports_external3.url().meta({ description: "LTI launch URL for this course" }).optional(),
49033
49110
  overrides: CourseOverrides3.optional()
49111
+ }).meta({
49112
+ id: "CourseConfig",
49113
+ description: "Configuration for a single course. Must have either grade or courseCode (or both)."
49034
49114
  });
49035
49115
  var TimebackConfig3 = exports_external3.object({
49036
- name: exports_external3.string().min(1, "App name is required"),
49037
- defaults: CourseDefaults3.optional(),
49038
- courses: exports_external3.array(CourseConfig3).min(1, "At least one course is required"),
49039
- sensor: exports_external3.string().url().optional(),
49040
- launchUrl: exports_external3.string().url().optional()
49116
+ $schema: exports_external3.string().meta({ description: "JSON Schema reference for editor support" }).optional(),
49117
+ name: exports_external3.string().min(1, "App name is required").meta({ description: "Display name for your app" }),
49118
+ defaults: CourseDefaults3.meta({
49119
+ description: "Default properties applied to all courses"
49120
+ }).optional(),
49121
+ courses: exports_external3.array(CourseConfig3).min(1, "At least one course is required").meta({ description: "Courses available in this app" }),
49122
+ sensor: exports_external3.url().meta({ description: "Default Caliper sensor endpoint URL for all courses" }).optional(),
49123
+ launchUrl: exports_external3.url().meta({ description: "Default LTI launch URL for all courses" }).optional()
49124
+ }).meta({
49125
+ id: "TimebackConfig",
49126
+ title: "Timeback Config",
49127
+ description: "Configuration schema for timeback.config.json files"
49041
49128
  }).refine((config22) => {
49042
49129
  return config22.courses.every((c) => c.grade !== undefined || c.courseCode !== undefined);
49043
49130
  }, {
@@ -49058,14 +49145,20 @@ var TimebackConfig3 = exports_external3.object({
49058
49145
  message: "Duplicate courseCode found; each must be unique",
49059
49146
  path: ["courses"]
49060
49147
  }).refine((config22) => {
49061
- return config22.courses.every((c) => c.sensor !== undefined || config22.sensor !== undefined);
49062
- }, {
49063
- message: "Each course must have an effective sensor; set a top-level `sensor` or per-course `sensor`",
49064
- path: ["courses"]
49065
- }).refine((config22) => {
49066
- return config22.courses.every((c) => c.launchUrl !== undefined || config22.launchUrl !== undefined);
49148
+ return config22.courses.every((c) => {
49149
+ if (c.sensor !== undefined || config22.sensor !== undefined) {
49150
+ return true;
49151
+ }
49152
+ const launchUrls = [
49153
+ c.launchUrl,
49154
+ config22.launchUrl,
49155
+ c.overrides?.staging?.launchUrl,
49156
+ c.overrides?.production?.launchUrl
49157
+ ].filter(Boolean);
49158
+ return launchUrls.length > 0;
49159
+ });
49067
49160
  }, {
49068
- message: "Each course must have an effective launchUrl; set a top-level `launchUrl` or per-course `launchUrl`",
49161
+ message: "Each course must have an effective sensor. Either set `sensor` explicitly (top-level or per-course), or provide a `launchUrl` so sensor can be derived from its origin.",
49069
49162
  path: ["courses"]
49070
49163
  });
49071
49164
  var EdubridgeDateString3 = exports_external3.union([IsoDateString3, IsoDateTimeString3]);
@@ -66342,7 +66435,7 @@ var TimebackSubject4 = exports_external4.enum([
66342
66435
  "Math",
66343
66436
  "None",
66344
66437
  "Other"
66345
- ]);
66438
+ ]).meta({ id: "TimebackSubject", description: "Subject area" });
66346
66439
  var TimebackGrade4 = exports_external4.union([
66347
66440
  exports_external4.literal(-1),
66348
66441
  exports_external4.literal(0),
@@ -66359,7 +66452,10 @@ var TimebackGrade4 = exports_external4.union([
66359
66452
  exports_external4.literal(11),
66360
66453
  exports_external4.literal(12),
66361
66454
  exports_external4.literal(13)
66362
- ]);
66455
+ ]).meta({
66456
+ id: "TimebackGrade",
66457
+ description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
66458
+ });
66363
66459
  var ScoreStatus4 = exports_external4.enum([
66364
66460
  "exempt",
66365
66461
  "fully graded",
@@ -66589,62 +66685,84 @@ var CaliperListEventsParams4 = exports_external4.object({
66589
66685
  actorEmail: exports_external4.email().optional()
66590
66686
  }).strict();
66591
66687
  var CourseIds4 = exports_external4.object({
66592
- staging: exports_external4.string().optional(),
66593
- production: exports_external4.string().optional()
66594
- });
66595
- var CourseType4 = exports_external4.enum(["base", "hole-filling", "optional"]);
66596
- var PublishStatus4 = exports_external4.enum(["draft", "testing", "published", "deactivated"]);
66688
+ staging: exports_external4.string().meta({ description: "Course ID in staging environment" }).optional(),
66689
+ production: exports_external4.string().meta({ description: "Course ID in production environment" }).optional()
66690
+ }).meta({ id: "CourseIds", description: "Environment-specific course IDs (populated by sync)" });
66691
+ var CourseType4 = exports_external4.enum(["base", "hole-filling", "optional"]).meta({ id: "CourseType", description: "Course classification type" });
66692
+ var PublishStatus4 = exports_external4.enum(["draft", "testing", "published", "deactivated"]).meta({ id: "PublishStatus", description: "Course publication status" });
66597
66693
  var CourseGoals4 = exports_external4.object({
66598
- dailyXp: exports_external4.number().int().positive().optional(),
66599
- dailyLessons: exports_external4.number().int().positive().optional(),
66600
- dailyActiveMinutes: exports_external4.number().int().positive().optional(),
66601
- dailyAccuracy: exports_external4.number().int().min(0).max(100).optional(),
66602
- dailyMasteredUnits: exports_external4.number().int().positive().optional()
66603
- });
66694
+ dailyXp: exports_external4.number().int().positive().meta({ description: "Target XP to earn per day" }).optional(),
66695
+ dailyLessons: exports_external4.number().int().positive().meta({ description: "Target lessons to complete per day" }).optional(),
66696
+ dailyActiveMinutes: exports_external4.number().int().positive().meta({ description: "Target active learning minutes per day" }).optional(),
66697
+ dailyAccuracy: exports_external4.number().int().min(0).max(100).meta({ description: "Target accuracy percentage (0-100)" }).optional(),
66698
+ dailyMasteredUnits: exports_external4.number().int().positive().meta({ description: "Target units to master per day" }).optional()
66699
+ }).meta({ id: "CourseGoals", description: "Daily learning goals for a course" });
66604
66700
  var CourseMetrics4 = exports_external4.object({
66605
- totalXp: exports_external4.number().int().positive().optional(),
66606
- totalLessons: exports_external4.number().int().positive().optional(),
66607
- totalGrades: exports_external4.number().int().positive().optional()
66608
- });
66701
+ totalXp: exports_external4.number().int().positive().meta({ description: "Total XP available in the course" }).optional(),
66702
+ totalLessons: exports_external4.number().int().positive().meta({ description: "Total number of lessons/activities" }).optional(),
66703
+ totalGrades: exports_external4.number().int().positive().meta({ description: "Total grade levels covered" }).optional()
66704
+ }).meta({ id: "CourseMetrics", description: "Aggregate metrics for a course" });
66609
66705
  var CourseMetadata4 = exports_external4.object({
66610
66706
  courseType: CourseType4.optional(),
66611
- isSupplemental: exports_external4.boolean().optional(),
66612
- isCustom: exports_external4.boolean().optional(),
66707
+ isSupplemental: exports_external4.boolean().meta({ description: "Whether this is supplemental to a base course" }).optional(),
66708
+ isCustom: exports_external4.boolean().meta({ description: "Whether this is a custom course for an individual student" }).optional(),
66613
66709
  publishStatus: PublishStatus4.optional(),
66614
- contactEmail: exports_external4.email().optional(),
66615
- primaryApp: exports_external4.string().optional(),
66710
+ contactEmail: exports_external4.email().meta({ description: "Contact email for course issues" }).optional(),
66711
+ primaryApp: exports_external4.string().meta({ description: "Primary application identifier" }).optional(),
66616
66712
  goals: CourseGoals4.optional(),
66617
66713
  metrics: CourseMetrics4.optional()
66618
- });
66714
+ }).meta({ id: "CourseMetadata", description: "Course metadata (matches API metadata object)" });
66619
66715
  var CourseDefaults4 = exports_external4.object({
66620
- courseCode: exports_external4.string().optional(),
66621
- level: exports_external4.string().optional(),
66716
+ courseCode: exports_external4.string().meta({ description: "Course code (e.g., 'MATH101')" }).optional(),
66717
+ level: exports_external4.string().meta({ description: "Course level (e.g., 'AP', 'Honors')" }).optional(),
66622
66718
  metadata: CourseMetadata4.optional()
66719
+ }).meta({
66720
+ id: "CourseDefaults",
66721
+ description: "Default properties that apply to all courses unless overridden"
66623
66722
  });
66624
66723
  var CourseEnvOverrides4 = exports_external4.object({
66625
- level: exports_external4.string().optional(),
66626
- sensor: exports_external4.string().url().optional(),
66627
- launchUrl: exports_external4.string().url().optional(),
66724
+ level: exports_external4.string().meta({ description: "Course level for this environment" }).optional(),
66725
+ sensor: exports_external4.url().meta({ description: "Caliper sensor endpoint URL for this environment" }).optional(),
66726
+ launchUrl: exports_external4.url().meta({ description: "LTI launch URL for this environment" }).optional(),
66628
66727
  metadata: CourseMetadata4.optional()
66728
+ }).meta({
66729
+ id: "CourseEnvOverrides",
66730
+ description: "Environment-specific course overrides (non-identity fields)"
66629
66731
  });
66630
66732
  var CourseOverrides4 = exports_external4.object({
66631
- staging: CourseEnvOverrides4.optional(),
66632
- production: CourseEnvOverrides4.optional()
66633
- });
66733
+ staging: CourseEnvOverrides4.meta({
66734
+ description: "Overrides for staging environment"
66735
+ }).optional(),
66736
+ production: CourseEnvOverrides4.meta({
66737
+ description: "Overrides for production environment"
66738
+ }).optional()
66739
+ }).meta({ id: "CourseOverrides", description: "Per-environment course overrides" });
66634
66740
  var CourseConfig4 = CourseDefaults4.extend({
66635
- subject: TimebackSubject4,
66636
- grade: TimebackGrade4.optional(),
66741
+ subject: TimebackSubject4.meta({ description: "Subject area for this course" }),
66742
+ grade: TimebackGrade4.meta({
66743
+ description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
66744
+ }).optional(),
66637
66745
  ids: CourseIds4.nullable().optional(),
66638
- sensor: exports_external4.string().url().optional(),
66639
- launchUrl: exports_external4.string().url().optional(),
66746
+ sensor: exports_external4.url().meta({ description: "Caliper sensor endpoint URL for this course" }).optional(),
66747
+ launchUrl: exports_external4.url().meta({ description: "LTI launch URL for this course" }).optional(),
66640
66748
  overrides: CourseOverrides4.optional()
66749
+ }).meta({
66750
+ id: "CourseConfig",
66751
+ description: "Configuration for a single course. Must have either grade or courseCode (or both)."
66641
66752
  });
66642
66753
  var TimebackConfig4 = exports_external4.object({
66643
- name: exports_external4.string().min(1, "App name is required"),
66644
- defaults: CourseDefaults4.optional(),
66645
- courses: exports_external4.array(CourseConfig4).min(1, "At least one course is required"),
66646
- sensor: exports_external4.string().url().optional(),
66647
- launchUrl: exports_external4.string().url().optional()
66754
+ $schema: exports_external4.string().meta({ description: "JSON Schema reference for editor support" }).optional(),
66755
+ name: exports_external4.string().min(1, "App name is required").meta({ description: "Display name for your app" }),
66756
+ defaults: CourseDefaults4.meta({
66757
+ description: "Default properties applied to all courses"
66758
+ }).optional(),
66759
+ courses: exports_external4.array(CourseConfig4).min(1, "At least one course is required").meta({ description: "Courses available in this app" }),
66760
+ sensor: exports_external4.url().meta({ description: "Default Caliper sensor endpoint URL for all courses" }).optional(),
66761
+ launchUrl: exports_external4.url().meta({ description: "Default LTI launch URL for all courses" }).optional()
66762
+ }).meta({
66763
+ id: "TimebackConfig",
66764
+ title: "Timeback Config",
66765
+ description: "Configuration schema for timeback.config.json files"
66648
66766
  }).refine((config22) => {
66649
66767
  return config22.courses.every((c) => c.grade !== undefined || c.courseCode !== undefined);
66650
66768
  }, {
@@ -66665,14 +66783,20 @@ var TimebackConfig4 = exports_external4.object({
66665
66783
  message: "Duplicate courseCode found; each must be unique",
66666
66784
  path: ["courses"]
66667
66785
  }).refine((config22) => {
66668
- return config22.courses.every((c) => c.sensor !== undefined || config22.sensor !== undefined);
66669
- }, {
66670
- message: "Each course must have an effective sensor; set a top-level `sensor` or per-course `sensor`",
66671
- path: ["courses"]
66672
- }).refine((config22) => {
66673
- return config22.courses.every((c) => c.launchUrl !== undefined || config22.launchUrl !== undefined);
66786
+ return config22.courses.every((c) => {
66787
+ if (c.sensor !== undefined || config22.sensor !== undefined) {
66788
+ return true;
66789
+ }
66790
+ const launchUrls = [
66791
+ c.launchUrl,
66792
+ config22.launchUrl,
66793
+ c.overrides?.staging?.launchUrl,
66794
+ c.overrides?.production?.launchUrl
66795
+ ].filter(Boolean);
66796
+ return launchUrls.length > 0;
66797
+ });
66674
66798
  }, {
66675
- message: "Each course must have an effective launchUrl; set a top-level `launchUrl` or per-course `launchUrl`",
66799
+ message: "Each course must have an effective sensor. Either set `sensor` explicitly (top-level or per-course), or provide a `launchUrl` so sensor can be derived from its origin.",
66676
66800
  path: ["courses"]
66677
66801
  });
66678
66802
  var EdubridgeDateString4 = exports_external4.union([IsoDateString4, IsoDateTimeString4]);
@@ -82979,7 +83103,7 @@ var TimebackSubject5 = exports_external5.enum([
82979
83103
  "Math",
82980
83104
  "None",
82981
83105
  "Other"
82982
- ]);
83106
+ ]).meta({ id: "TimebackSubject", description: "Subject area" });
82983
83107
  var TimebackGrade5 = exports_external5.union([
82984
83108
  exports_external5.literal(-1),
82985
83109
  exports_external5.literal(0),
@@ -82996,7 +83120,10 @@ var TimebackGrade5 = exports_external5.union([
82996
83120
  exports_external5.literal(11),
82997
83121
  exports_external5.literal(12),
82998
83122
  exports_external5.literal(13)
82999
- ]);
83123
+ ]).meta({
83124
+ id: "TimebackGrade",
83125
+ description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
83126
+ });
83000
83127
  var ScoreStatus5 = exports_external5.enum([
83001
83128
  "exempt",
83002
83129
  "fully graded",
@@ -83226,62 +83353,84 @@ var CaliperListEventsParams5 = exports_external5.object({
83226
83353
  actorEmail: exports_external5.email().optional()
83227
83354
  }).strict();
83228
83355
  var CourseIds5 = exports_external5.object({
83229
- staging: exports_external5.string().optional(),
83230
- production: exports_external5.string().optional()
83231
- });
83232
- var CourseType5 = exports_external5.enum(["base", "hole-filling", "optional"]);
83233
- var PublishStatus5 = exports_external5.enum(["draft", "testing", "published", "deactivated"]);
83356
+ staging: exports_external5.string().meta({ description: "Course ID in staging environment" }).optional(),
83357
+ production: exports_external5.string().meta({ description: "Course ID in production environment" }).optional()
83358
+ }).meta({ id: "CourseIds", description: "Environment-specific course IDs (populated by sync)" });
83359
+ var CourseType5 = exports_external5.enum(["base", "hole-filling", "optional"]).meta({ id: "CourseType", description: "Course classification type" });
83360
+ var PublishStatus5 = exports_external5.enum(["draft", "testing", "published", "deactivated"]).meta({ id: "PublishStatus", description: "Course publication status" });
83234
83361
  var CourseGoals5 = exports_external5.object({
83235
- dailyXp: exports_external5.number().int().positive().optional(),
83236
- dailyLessons: exports_external5.number().int().positive().optional(),
83237
- dailyActiveMinutes: exports_external5.number().int().positive().optional(),
83238
- dailyAccuracy: exports_external5.number().int().min(0).max(100).optional(),
83239
- dailyMasteredUnits: exports_external5.number().int().positive().optional()
83240
- });
83362
+ dailyXp: exports_external5.number().int().positive().meta({ description: "Target XP to earn per day" }).optional(),
83363
+ dailyLessons: exports_external5.number().int().positive().meta({ description: "Target lessons to complete per day" }).optional(),
83364
+ dailyActiveMinutes: exports_external5.number().int().positive().meta({ description: "Target active learning minutes per day" }).optional(),
83365
+ dailyAccuracy: exports_external5.number().int().min(0).max(100).meta({ description: "Target accuracy percentage (0-100)" }).optional(),
83366
+ dailyMasteredUnits: exports_external5.number().int().positive().meta({ description: "Target units to master per day" }).optional()
83367
+ }).meta({ id: "CourseGoals", description: "Daily learning goals for a course" });
83241
83368
  var CourseMetrics5 = exports_external5.object({
83242
- totalXp: exports_external5.number().int().positive().optional(),
83243
- totalLessons: exports_external5.number().int().positive().optional(),
83244
- totalGrades: exports_external5.number().int().positive().optional()
83245
- });
83369
+ totalXp: exports_external5.number().int().positive().meta({ description: "Total XP available in the course" }).optional(),
83370
+ totalLessons: exports_external5.number().int().positive().meta({ description: "Total number of lessons/activities" }).optional(),
83371
+ totalGrades: exports_external5.number().int().positive().meta({ description: "Total grade levels covered" }).optional()
83372
+ }).meta({ id: "CourseMetrics", description: "Aggregate metrics for a course" });
83246
83373
  var CourseMetadata5 = exports_external5.object({
83247
83374
  courseType: CourseType5.optional(),
83248
- isSupplemental: exports_external5.boolean().optional(),
83249
- isCustom: exports_external5.boolean().optional(),
83375
+ isSupplemental: exports_external5.boolean().meta({ description: "Whether this is supplemental to a base course" }).optional(),
83376
+ isCustom: exports_external5.boolean().meta({ description: "Whether this is a custom course for an individual student" }).optional(),
83250
83377
  publishStatus: PublishStatus5.optional(),
83251
- contactEmail: exports_external5.email().optional(),
83252
- primaryApp: exports_external5.string().optional(),
83378
+ contactEmail: exports_external5.email().meta({ description: "Contact email for course issues" }).optional(),
83379
+ primaryApp: exports_external5.string().meta({ description: "Primary application identifier" }).optional(),
83253
83380
  goals: CourseGoals5.optional(),
83254
83381
  metrics: CourseMetrics5.optional()
83255
- });
83382
+ }).meta({ id: "CourseMetadata", description: "Course metadata (matches API metadata object)" });
83256
83383
  var CourseDefaults5 = exports_external5.object({
83257
- courseCode: exports_external5.string().optional(),
83258
- level: exports_external5.string().optional(),
83384
+ courseCode: exports_external5.string().meta({ description: "Course code (e.g., 'MATH101')" }).optional(),
83385
+ level: exports_external5.string().meta({ description: "Course level (e.g., 'AP', 'Honors')" }).optional(),
83259
83386
  metadata: CourseMetadata5.optional()
83387
+ }).meta({
83388
+ id: "CourseDefaults",
83389
+ description: "Default properties that apply to all courses unless overridden"
83260
83390
  });
83261
83391
  var CourseEnvOverrides5 = exports_external5.object({
83262
- level: exports_external5.string().optional(),
83263
- sensor: exports_external5.string().url().optional(),
83264
- launchUrl: exports_external5.string().url().optional(),
83392
+ level: exports_external5.string().meta({ description: "Course level for this environment" }).optional(),
83393
+ sensor: exports_external5.url().meta({ description: "Caliper sensor endpoint URL for this environment" }).optional(),
83394
+ launchUrl: exports_external5.url().meta({ description: "LTI launch URL for this environment" }).optional(),
83265
83395
  metadata: CourseMetadata5.optional()
83396
+ }).meta({
83397
+ id: "CourseEnvOverrides",
83398
+ description: "Environment-specific course overrides (non-identity fields)"
83266
83399
  });
83267
83400
  var CourseOverrides5 = exports_external5.object({
83268
- staging: CourseEnvOverrides5.optional(),
83269
- production: CourseEnvOverrides5.optional()
83270
- });
83401
+ staging: CourseEnvOverrides5.meta({
83402
+ description: "Overrides for staging environment"
83403
+ }).optional(),
83404
+ production: CourseEnvOverrides5.meta({
83405
+ description: "Overrides for production environment"
83406
+ }).optional()
83407
+ }).meta({ id: "CourseOverrides", description: "Per-environment course overrides" });
83271
83408
  var CourseConfig5 = CourseDefaults5.extend({
83272
- subject: TimebackSubject5,
83273
- grade: TimebackGrade5.optional(),
83409
+ subject: TimebackSubject5.meta({ description: "Subject area for this course" }),
83410
+ grade: TimebackGrade5.meta({
83411
+ description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
83412
+ }).optional(),
83274
83413
  ids: CourseIds5.nullable().optional(),
83275
- sensor: exports_external5.string().url().optional(),
83276
- launchUrl: exports_external5.string().url().optional(),
83414
+ sensor: exports_external5.url().meta({ description: "Caliper sensor endpoint URL for this course" }).optional(),
83415
+ launchUrl: exports_external5.url().meta({ description: "LTI launch URL for this course" }).optional(),
83277
83416
  overrides: CourseOverrides5.optional()
83417
+ }).meta({
83418
+ id: "CourseConfig",
83419
+ description: "Configuration for a single course. Must have either grade or courseCode (or both)."
83278
83420
  });
83279
83421
  var TimebackConfig5 = exports_external5.object({
83280
- name: exports_external5.string().min(1, "App name is required"),
83281
- defaults: CourseDefaults5.optional(),
83282
- courses: exports_external5.array(CourseConfig5).min(1, "At least one course is required"),
83283
- sensor: exports_external5.string().url().optional(),
83284
- launchUrl: exports_external5.string().url().optional()
83422
+ $schema: exports_external5.string().meta({ description: "JSON Schema reference for editor support" }).optional(),
83423
+ name: exports_external5.string().min(1, "App name is required").meta({ description: "Display name for your app" }),
83424
+ defaults: CourseDefaults5.meta({
83425
+ description: "Default properties applied to all courses"
83426
+ }).optional(),
83427
+ courses: exports_external5.array(CourseConfig5).min(1, "At least one course is required").meta({ description: "Courses available in this app" }),
83428
+ sensor: exports_external5.url().meta({ description: "Default Caliper sensor endpoint URL for all courses" }).optional(),
83429
+ launchUrl: exports_external5.url().meta({ description: "Default LTI launch URL for all courses" }).optional()
83430
+ }).meta({
83431
+ id: "TimebackConfig",
83432
+ title: "Timeback Config",
83433
+ description: "Configuration schema for timeback.config.json files"
83285
83434
  }).refine((config22) => {
83286
83435
  return config22.courses.every((c) => c.grade !== undefined || c.courseCode !== undefined);
83287
83436
  }, {
@@ -83302,14 +83451,20 @@ var TimebackConfig5 = exports_external5.object({
83302
83451
  message: "Duplicate courseCode found; each must be unique",
83303
83452
  path: ["courses"]
83304
83453
  }).refine((config22) => {
83305
- return config22.courses.every((c) => c.sensor !== undefined || config22.sensor !== undefined);
83306
- }, {
83307
- message: "Each course must have an effective sensor; set a top-level `sensor` or per-course `sensor`",
83308
- path: ["courses"]
83309
- }).refine((config22) => {
83310
- return config22.courses.every((c) => c.launchUrl !== undefined || config22.launchUrl !== undefined);
83454
+ return config22.courses.every((c) => {
83455
+ if (c.sensor !== undefined || config22.sensor !== undefined) {
83456
+ return true;
83457
+ }
83458
+ const launchUrls = [
83459
+ c.launchUrl,
83460
+ config22.launchUrl,
83461
+ c.overrides?.staging?.launchUrl,
83462
+ c.overrides?.production?.launchUrl
83463
+ ].filter(Boolean);
83464
+ return launchUrls.length > 0;
83465
+ });
83311
83466
  }, {
83312
- message: "Each course must have an effective launchUrl; set a top-level `launchUrl` or per-course `launchUrl`",
83467
+ message: "Each course must have an effective sensor. Either set `sensor` explicitly (top-level or per-course), or provide a `launchUrl` so sensor can be derived from its origin.",
83313
83468
  path: ["courses"]
83314
83469
  });
83315
83470
  var EdubridgeDateString5 = exports_external5.union([IsoDateString5, IsoDateTimeString5]);
@@ -85060,8 +85215,8 @@ var {
85060
85215
 
85061
85216
  // ../internal/cli-infra/src/config/timeback.ts
85062
85217
  import { existsSync } from "node:fs";
85063
- import { basename, relative, resolve } from "node:path";
85064
- import { pathToFileURL } from "node:url";
85218
+ import { basename, extname, relative, resolve } from "node:path";
85219
+ import { loadConfig as c12LoadConfig } from "c12";
85065
85220
 
85066
85221
  // ../types/src/zod/primitives.ts
85067
85222
  import { z as z6 } from "zod/v4";
@@ -85079,7 +85234,7 @@ var TimebackSubject6 = z6.enum([
85079
85234
  "Math",
85080
85235
  "None",
85081
85236
  "Other"
85082
- ]);
85237
+ ]).meta({ id: "TimebackSubject", description: "Subject area" });
85083
85238
  var TimebackGrade6 = z6.union([
85084
85239
  z6.literal(-1),
85085
85240
  z6.literal(0),
@@ -85096,7 +85251,10 @@ var TimebackGrade6 = z6.union([
85096
85251
  z6.literal(11),
85097
85252
  z6.literal(12),
85098
85253
  z6.literal(13)
85099
- ]);
85254
+ ]).meta({
85255
+ id: "TimebackGrade",
85256
+ description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
85257
+ });
85100
85258
  var ScoreStatus6 = z6.enum([
85101
85259
  "exempt",
85102
85260
  "fully graded",
@@ -85330,62 +85488,84 @@ var CaliperListEventsParams6 = z8.object({
85330
85488
  // ../types/src/zod/config.ts
85331
85489
  import { z as z9 } from "zod/v4";
85332
85490
  var CourseIds6 = z9.object({
85333
- staging: z9.string().optional(),
85334
- production: z9.string().optional()
85335
- });
85336
- var CourseType6 = z9.enum(["base", "hole-filling", "optional"]);
85337
- var PublishStatus6 = z9.enum(["draft", "testing", "published", "deactivated"]);
85491
+ staging: z9.string().meta({ description: "Course ID in staging environment" }).optional(),
85492
+ production: z9.string().meta({ description: "Course ID in production environment" }).optional()
85493
+ }).meta({ id: "CourseIds", description: "Environment-specific course IDs (populated by sync)" });
85494
+ var CourseType6 = z9.enum(["base", "hole-filling", "optional"]).meta({ id: "CourseType", description: "Course classification type" });
85495
+ var PublishStatus6 = z9.enum(["draft", "testing", "published", "deactivated"]).meta({ id: "PublishStatus", description: "Course publication status" });
85338
85496
  var CourseGoals6 = z9.object({
85339
- dailyXp: z9.number().int().positive().optional(),
85340
- dailyLessons: z9.number().int().positive().optional(),
85341
- dailyActiveMinutes: z9.number().int().positive().optional(),
85342
- dailyAccuracy: z9.number().int().min(0).max(100).optional(),
85343
- dailyMasteredUnits: z9.number().int().positive().optional()
85344
- });
85497
+ dailyXp: z9.number().int().positive().meta({ description: "Target XP to earn per day" }).optional(),
85498
+ dailyLessons: z9.number().int().positive().meta({ description: "Target lessons to complete per day" }).optional(),
85499
+ dailyActiveMinutes: z9.number().int().positive().meta({ description: "Target active learning minutes per day" }).optional(),
85500
+ dailyAccuracy: z9.number().int().min(0).max(100).meta({ description: "Target accuracy percentage (0-100)" }).optional(),
85501
+ dailyMasteredUnits: z9.number().int().positive().meta({ description: "Target units to master per day" }).optional()
85502
+ }).meta({ id: "CourseGoals", description: "Daily learning goals for a course" });
85345
85503
  var CourseMetrics6 = z9.object({
85346
- totalXp: z9.number().int().positive().optional(),
85347
- totalLessons: z9.number().int().positive().optional(),
85348
- totalGrades: z9.number().int().positive().optional()
85349
- });
85504
+ totalXp: z9.number().int().positive().meta({ description: "Total XP available in the course" }).optional(),
85505
+ totalLessons: z9.number().int().positive().meta({ description: "Total number of lessons/activities" }).optional(),
85506
+ totalGrades: z9.number().int().positive().meta({ description: "Total grade levels covered" }).optional()
85507
+ }).meta({ id: "CourseMetrics", description: "Aggregate metrics for a course" });
85350
85508
  var CourseMetadata6 = z9.object({
85351
85509
  courseType: CourseType6.optional(),
85352
- isSupplemental: z9.boolean().optional(),
85353
- isCustom: z9.boolean().optional(),
85510
+ isSupplemental: z9.boolean().meta({ description: "Whether this is supplemental to a base course" }).optional(),
85511
+ isCustom: z9.boolean().meta({ description: "Whether this is a custom course for an individual student" }).optional(),
85354
85512
  publishStatus: PublishStatus6.optional(),
85355
- contactEmail: z9.email().optional(),
85356
- primaryApp: z9.string().optional(),
85513
+ contactEmail: z9.email().meta({ description: "Contact email for course issues" }).optional(),
85514
+ primaryApp: z9.string().meta({ description: "Primary application identifier" }).optional(),
85357
85515
  goals: CourseGoals6.optional(),
85358
85516
  metrics: CourseMetrics6.optional()
85359
- });
85517
+ }).meta({ id: "CourseMetadata", description: "Course metadata (matches API metadata object)" });
85360
85518
  var CourseDefaults6 = z9.object({
85361
- courseCode: z9.string().optional(),
85362
- level: z9.string().optional(),
85519
+ courseCode: z9.string().meta({ description: "Course code (e.g., 'MATH101')" }).optional(),
85520
+ level: z9.string().meta({ description: "Course level (e.g., 'AP', 'Honors')" }).optional(),
85363
85521
  metadata: CourseMetadata6.optional()
85522
+ }).meta({
85523
+ id: "CourseDefaults",
85524
+ description: "Default properties that apply to all courses unless overridden"
85364
85525
  });
85365
85526
  var CourseEnvOverrides6 = z9.object({
85366
- level: z9.string().optional(),
85367
- sensor: z9.string().url().optional(),
85368
- launchUrl: z9.string().url().optional(),
85527
+ level: z9.string().meta({ description: "Course level for this environment" }).optional(),
85528
+ sensor: z9.url().meta({ description: "Caliper sensor endpoint URL for this environment" }).optional(),
85529
+ launchUrl: z9.url().meta({ description: "LTI launch URL for this environment" }).optional(),
85369
85530
  metadata: CourseMetadata6.optional()
85531
+ }).meta({
85532
+ id: "CourseEnvOverrides",
85533
+ description: "Environment-specific course overrides (non-identity fields)"
85370
85534
  });
85371
85535
  var CourseOverrides6 = z9.object({
85372
- staging: CourseEnvOverrides6.optional(),
85373
- production: CourseEnvOverrides6.optional()
85374
- });
85536
+ staging: CourseEnvOverrides6.meta({
85537
+ description: "Overrides for staging environment"
85538
+ }).optional(),
85539
+ production: CourseEnvOverrides6.meta({
85540
+ description: "Overrides for production environment"
85541
+ }).optional()
85542
+ }).meta({ id: "CourseOverrides", description: "Per-environment course overrides" });
85375
85543
  var CourseConfig6 = CourseDefaults6.extend({
85376
- subject: TimebackSubject6,
85377
- grade: TimebackGrade6.optional(),
85544
+ subject: TimebackSubject6.meta({ description: "Subject area for this course" }),
85545
+ grade: TimebackGrade6.meta({
85546
+ description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
85547
+ }).optional(),
85378
85548
  ids: CourseIds6.nullable().optional(),
85379
- sensor: z9.string().url().optional(),
85380
- launchUrl: z9.string().url().optional(),
85549
+ sensor: z9.url().meta({ description: "Caliper sensor endpoint URL for this course" }).optional(),
85550
+ launchUrl: z9.url().meta({ description: "LTI launch URL for this course" }).optional(),
85381
85551
  overrides: CourseOverrides6.optional()
85552
+ }).meta({
85553
+ id: "CourseConfig",
85554
+ description: "Configuration for a single course. Must have either grade or courseCode (or both)."
85382
85555
  });
85383
85556
  var TimebackConfig6 = z9.object({
85384
- name: z9.string().min(1, "App name is required"),
85385
- defaults: CourseDefaults6.optional(),
85386
- courses: z9.array(CourseConfig6).min(1, "At least one course is required"),
85387
- sensor: z9.string().url().optional(),
85388
- launchUrl: z9.string().url().optional()
85557
+ $schema: z9.string().meta({ description: "JSON Schema reference for editor support" }).optional(),
85558
+ name: z9.string().min(1, "App name is required").meta({ description: "Display name for your app" }),
85559
+ defaults: CourseDefaults6.meta({
85560
+ description: "Default properties applied to all courses"
85561
+ }).optional(),
85562
+ courses: z9.array(CourseConfig6).min(1, "At least one course is required").meta({ description: "Courses available in this app" }),
85563
+ sensor: z9.url().meta({ description: "Default Caliper sensor endpoint URL for all courses" }).optional(),
85564
+ launchUrl: z9.url().meta({ description: "Default LTI launch URL for all courses" }).optional()
85565
+ }).meta({
85566
+ id: "TimebackConfig",
85567
+ title: "Timeback Config",
85568
+ description: "Configuration schema for timeback.config.json files"
85389
85569
  }).refine((config6) => {
85390
85570
  return config6.courses.every((c) => c.grade !== undefined || c.courseCode !== undefined);
85391
85571
  }, {
@@ -85406,14 +85586,20 @@ var TimebackConfig6 = z9.object({
85406
85586
  message: "Duplicate courseCode found; each must be unique",
85407
85587
  path: ["courses"]
85408
85588
  }).refine((config6) => {
85409
- return config6.courses.every((c) => c.sensor !== undefined || config6.sensor !== undefined);
85410
- }, {
85411
- message: "Each course must have an effective sensor; set a top-level `sensor` or per-course `sensor`",
85412
- path: ["courses"]
85413
- }).refine((config6) => {
85414
- return config6.courses.every((c) => c.launchUrl !== undefined || config6.launchUrl !== undefined);
85589
+ return config6.courses.every((c) => {
85590
+ if (c.sensor !== undefined || config6.sensor !== undefined) {
85591
+ return true;
85592
+ }
85593
+ const launchUrls = [
85594
+ c.launchUrl,
85595
+ config6.launchUrl,
85596
+ c.overrides?.staging?.launchUrl,
85597
+ c.overrides?.production?.launchUrl
85598
+ ].filter(Boolean);
85599
+ return launchUrls.length > 0;
85600
+ });
85415
85601
  }, {
85416
- message: "Each course must have an effective launchUrl; set a top-level `launchUrl` or per-course `launchUrl`",
85602
+ message: "Each course must have an effective sensor. Either set `sensor` explicitly (top-level or per-course), or provide a `launchUrl` so sensor can be derived from its origin.",
85417
85603
  path: ["courses"]
85418
85604
  });
85419
85605
  // ../types/src/zod/edubridge.ts
@@ -86433,108 +86619,89 @@ var CREDENTIALS_DIR = getConfigDir();
86433
86619
  var CREDENTIALS_FILE = join(CREDENTIALS_DIR, "credentials.json");
86434
86620
 
86435
86621
  // ../internal/cli-infra/src/config/timeback.ts
86436
- var FILE_PATTERNS = ["timeback.config.ts", "timeback.config.js", "timeback.config.mjs"];
86437
- var isBun = typeof globalThis.Bun !== "undefined";
86438
- var jitiInstance = null;
86439
- async function getJiti() {
86440
- if (jitiInstance)
86441
- return jitiInstance;
86442
- const { createJiti } = await import("jiti");
86443
- jitiInstance = createJiti(import.meta.url, { fsCache: false });
86444
- return jitiInstance;
86445
- }
86446
- async function importModule(fullPath) {
86447
- let module;
86448
- if (isBun) {
86449
- const fileUrl = pathToFileURL(fullPath).href;
86450
- module = await import(fileUrl);
86451
- } else {
86452
- const jiti = await getJiti();
86453
- module = await jiti.import(fullPath);
86454
- }
86455
- return module.default ?? module;
86622
+ var CONFIG_FILENAME = "timeback.config.json";
86623
+ function isJsonConfigPath(configPath) {
86624
+ return extname(configPath).toLowerCase() === ".json";
86625
+ }
86626
+ async function loadWithC12(cwd, configPath) {
86627
+ if (configPath && !isJsonConfigPath(configPath)) {
86628
+ throw new Error(`Config file must be JSON (.json): ${configPath}`);
86629
+ }
86630
+ const result = await c12LoadConfig({
86631
+ cwd,
86632
+ name: "timeback",
86633
+ configFile: configPath ?? CONFIG_FILENAME,
86634
+ rcFile: false,
86635
+ packageJson: false,
86636
+ dotenv: false,
86637
+ envName: false,
86638
+ extend: false,
86639
+ omit$Keys: true,
86640
+ defaults: {},
86641
+ overrides: {}
86642
+ });
86643
+ if (!result.config || Object.keys(result.config).length === 0) {
86644
+ return null;
86645
+ }
86646
+ const rawConfig = result.config;
86647
+ if ("extends" in rawConfig) {
86648
+ throw new Error("The 'extends' feature is not supported in timeback.config.json. " + "Please inline all configuration.");
86649
+ }
86650
+ const { $schema: _schema, ...configWithoutSchema } = rawConfig;
86651
+ return {
86652
+ config: configWithoutSchema,
86653
+ configFile: result.configFile ?? resolve(cwd, configPath ?? CONFIG_FILENAME)
86654
+ };
86456
86655
  }
86457
86656
  async function loadConfig(opts = {}) {
86458
86657
  const cwd = process.cwd();
86459
- let rawConfig = null;
86460
- let foundPath = null;
86461
- let loadError = null;
86462
- if (opts.configPath) {
86463
- const fullPath = resolve(cwd, opts.configPath);
86464
- if (!existsSync(fullPath)) {
86658
+ try {
86659
+ if (opts.configPath && !isJsonConfigPath(opts.configPath)) {
86465
86660
  return {
86466
86661
  success: false,
86467
- error: `Config file not found: ${opts.configPath}`
86662
+ error: `Config file must be JSON (.json): ${opts.configPath}`
86468
86663
  };
86469
86664
  }
86470
- try {
86471
- rawConfig = await importModule(fullPath);
86472
- foundPath = fullPath;
86473
- } catch (err) {
86665
+ if (opts.configPath) {
86666
+ const fullPath = resolve(cwd, opts.configPath);
86667
+ if (!existsSync(fullPath)) {
86668
+ return {
86669
+ success: false,
86670
+ error: `Config file not found: ${opts.configPath}`
86671
+ };
86672
+ }
86673
+ }
86674
+ const loaded = await loadWithC12(cwd, opts.configPath);
86675
+ if (!loaded) {
86474
86676
  return {
86475
86677
  success: false,
86476
- error: `Failed to load ${opts.configPath}:
86477
- ${err instanceof Error ? err.message : String(err)}`
86678
+ error: `No timeback config found. Run ${greenBright("timeback init")} to create one.`
86478
86679
  };
86479
86680
  }
86480
- } else {
86481
- for (const filename of FILE_PATTERNS) {
86482
- const fullPath = resolve(cwd, filename);
86483
- if (!existsSync(fullPath))
86484
- continue;
86485
- try {
86486
- rawConfig = await importModule(fullPath);
86487
- foundPath = fullPath;
86488
- break;
86489
- } catch (err) {
86490
- loadError = err instanceof Error ? err : new Error(String(err));
86491
- foundPath = fullPath;
86492
- break;
86493
- }
86681
+ const result = TimebackConfig6.safeParse(loaded.config);
86682
+ if (!result.success) {
86683
+ const issues = result.error.issues.map((issue4) => ` - ${issue4.path.join(".")}: ${issue4.message}`).join(`
86684
+ `);
86685
+ return {
86686
+ success: false,
86687
+ error: `Invalid config in ${basename(loaded.configFile)}:
86688
+ ${issues}`
86689
+ };
86494
86690
  }
86495
- }
86496
- if (loadError && foundPath) {
86497
86691
  return {
86498
- success: false,
86499
- error: `Failed to load ${basename(foundPath)}:
86500
- ${loadError.message}`
86692
+ success: true,
86693
+ config: result.data,
86694
+ configPath: loaded.configFile
86501
86695
  };
86502
- }
86503
- if (!rawConfig || !foundPath) {
86504
- return {
86505
- success: false,
86506
- error: `No timeback config found. Run ${greenBright("timeback init")} to create one.`
86507
- };
86508
- }
86509
- const result = TimebackConfig6.safeParse(rawConfig);
86510
- if (!result.success) {
86511
- const issues = result.error.issues.map((issue4) => ` - ${issue4.path.join(".")}: ${issue4.message}`).join(`
86512
- `);
86696
+ } catch (err) {
86697
+ const filename = opts.configPath ? basename(opts.configPath) : CONFIG_FILENAME;
86513
86698
  return {
86514
86699
  success: false,
86515
- error: `Invalid config in ${basename(foundPath)}:
86516
- ${issues}`
86700
+ error: `Failed to load ${filename}:
86701
+ ${err instanceof Error ? err.message : String(err)}`
86517
86702
  };
86518
86703
  }
86519
- return {
86520
- success: true,
86521
- config: result.data,
86522
- configPath: foundPath
86523
- };
86524
86704
  }
86525
- // src/shared/constants.ts
86526
- var ROUTES = {
86527
- ACTIVITY: "/activity",
86528
- IDENTITY: {
86529
- SIGNIN: "/identity/signin",
86530
- SIGNOUT: "/identity/signout",
86531
- CALLBACK: "/identity/callback"
86532
- },
86533
- USER: {
86534
- ME: "/user/me"
86535
- }
86536
- };
86537
-
86538
86705
  // ../internal/logger/src/debug.ts
86539
86706
  var patterns7 = null;
86540
86707
  var debugAll7 = false;
@@ -86952,12 +87119,32 @@ async function getUserInfo(params) {
86952
87119
  return response.json();
86953
87120
  }
86954
87121
  // src/server/lib/utils.ts
87122
+ function safeIdSegment(value) {
87123
+ return encodeURIComponent(value).replace(/%/g, "_");
87124
+ }
87125
+ function hashSuffix64Base36(value) {
87126
+ let hash6 = 0xcbf29ce484222325n;
87127
+ const prime = 0x100000001b3n;
87128
+ const mod64 = 0xffffffffffffffffn;
87129
+ for (let i = 0;i < value.length; i++) {
87130
+ hash6 ^= BigInt(value.charCodeAt(i));
87131
+ hash6 = hash6 * prime & mod64;
87132
+ }
87133
+ const base36 = hash6.toString(36);
87134
+ return base36.length > 12 ? base36.slice(-12) : base36;
87135
+ }
86955
87136
  function mapEnvForApi(env2) {
86956
87137
  if (env2 === "local" || env2 === "staging") {
86957
87138
  return "staging";
86958
87139
  }
86959
87140
  return "production";
86960
87141
  }
87142
+ function normalizeEnv(env2) {
87143
+ if (env2 === "production" || env2 === "local" || env2 === "staging") {
87144
+ return env2;
87145
+ }
87146
+ return "staging";
87147
+ }
86961
87148
  function jsonResponse(data, status = 200, headers) {
86962
87149
  const responseHeaders = new Headers(headers);
86963
87150
  responseHeaders.set("Content-Type", "application/json");
@@ -87092,164 +87279,21 @@ async function lookupTimebackIdByEmail(params) {
87092
87279
  throw new TimebackUserResolutionError(`Failed to lookup Timeback user: ${message}`, "timeback_user_lookup_failed");
87093
87280
  }
87094
87281
  }
87095
-
87096
- class ActivityCourseResolutionError extends Error {
87097
- code;
87098
- selector;
87099
- count;
87100
- constructor(code, selector, count) {
87101
- super(code);
87102
- this.code = code;
87103
- this.selector = selector;
87104
- this.count = count;
87105
- }
87106
- get selectorDescription() {
87107
- if ("grade" in this.selector) {
87108
- return `${this.selector.subject} grade ${this.selector.grade}`;
87109
- }
87110
- return `code "${this.selector.code}"`;
87111
- }
87112
- }
87113
- function resolveActivityCourse(courses, courseRef) {
87114
- let matches;
87115
- if ("grade" in courseRef) {
87116
- matches = courses.filter((c) => c.subject === courseRef.subject && c.grade === courseRef.grade);
87117
- } else {
87118
- matches = courses.filter((c) => c.courseCode === courseRef.code);
87119
- }
87120
- if (matches.length === 0) {
87121
- throw new ActivityCourseResolutionError("unknown_course", courseRef);
87122
- }
87123
- if (matches.length > 1) {
87124
- throw new ActivityCourseResolutionError("ambiguous_course", courseRef, matches.length);
87125
- }
87126
- return matches[0];
87127
- }
87128
- // src/server/lib/build-activity-events.ts
87129
- class MissingSyncedCourseIdError extends Error {
87130
- course;
87131
- env;
87132
- constructor(course, env2) {
87133
- const identifier = course.grade === undefined ? course.courseCode ?? course.subject : `${course.subject} grade ${course.grade}`;
87134
- super(`Course "${identifier}" is missing a synced ID for ${env2}. Run \`timeback sync\` first.`);
87135
- this.name = "MissingSyncedCourseIdError";
87136
- this.course = course;
87137
- this.env = env2;
87138
- }
87139
- }
87140
- function buildCourseId(course, apiEnv) {
87141
- const courseId = course.ids?.[apiEnv];
87142
- if (!courseId) {
87143
- throw new MissingSyncedCourseIdError(course, apiEnv);
87144
- }
87145
- return courseId;
87146
- }
87147
- function buildCourseName(course) {
87148
- if (course.courseCode) {
87149
- return course.courseCode;
87150
- }
87151
- if (course.grade !== undefined) {
87152
- return `${course.subject} G${String(course.grade)}`;
87282
+ // src/shared/constants.ts
87283
+ var ROUTES = {
87284
+ ACTIVITY: "/activity",
87285
+ IDENTITY: {
87286
+ SIGNIN: "/identity/signin",
87287
+ SIGNOUT: "/identity/signout",
87288
+ CALLBACK: "/identity/callback"
87289
+ },
87290
+ USER: {
87291
+ ME: "/user/me",
87292
+ VERIFY: "/user/verify"
87153
87293
  }
87154
- return course.subject;
87155
- }
87294
+ };
87156
87295
 
87157
- class InvalidSensorUrlError extends Error {
87158
- sensor;
87159
- constructor(sensor) {
87160
- super(`Invalid sensor URL "${sensor}". Sensor must be a valid absolute URL (e.g., "https://sensor.example.com") to support slug-based activity IDs.`);
87161
- this.name = "InvalidSensorUrlError";
87162
- this.sensor = sensor;
87163
- }
87164
- }
87165
- function buildCanonicalActivityUrl(sensor, selector, slug) {
87166
- let base;
87167
- try {
87168
- base = new URL(sensor);
87169
- } catch {
87170
- throw new InvalidSensorUrlError(sensor);
87171
- }
87172
- const pathSegment = "grade" in selector ? `${selector.subject}/g${String(selector.grade)}` : selector.code;
87173
- const basePath = base.pathname.replace(/\/+$/, "");
87174
- base.pathname = `${basePath}/activities/${pathSegment}/${encodeURIComponent(slug)}`;
87175
- return base.toString();
87176
- }
87177
- function buildActivityContext(payload, course, appName, apiEnv, sensor) {
87178
- return {
87179
- id: buildCanonicalActivityUrl(sensor, payload.course, payload.id),
87180
- type: "TimebackActivityContext",
87181
- subject: course.subject,
87182
- app: { name: appName },
87183
- activity: { name: payload.name },
87184
- course: {
87185
- id: buildCourseId(course, apiEnv),
87186
- name: buildCourseName(course)
87187
- }
87188
- };
87189
- }
87190
- function buildActivityMetrics(metrics) {
87191
- const result = [];
87192
- if (metrics.totalQuestions !== undefined) {
87193
- result.push({ type: "totalQuestions", value: metrics.totalQuestions });
87194
- }
87195
- if (metrics.correctQuestions !== undefined) {
87196
- result.push({ type: "correctQuestions", value: metrics.correctQuestions });
87197
- }
87198
- if (metrics.xpEarned !== undefined) {
87199
- result.push({ type: "xpEarned", value: metrics.xpEarned });
87200
- }
87201
- if (metrics.masteredUnits !== undefined) {
87202
- result.push({ type: "masteredUnits", value: metrics.masteredUnits });
87203
- }
87204
- return result;
87205
- }
87206
- function buildTimeSpentMetrics(elapsedMs, pausedMs) {
87207
- const result = [{ type: "active", value: Math.max(0, elapsedMs) / 1000 }];
87208
- if (pausedMs > 0) {
87209
- result.push({ type: "inactive", value: Math.max(0, pausedMs) / 1000 });
87210
- }
87211
- return result;
87212
- }
87213
- async function sendCaliperEnvelope(client, sensor, activityEvent, timeSpentEvent) {
87214
- await client.caliper.events.send(sensor, [activityEvent, timeSpentEvent]);
87215
- }
87216
- // src/server/lib/build-user-profile.ts
87217
- function buildCourseLookup(courses, apiEnv) {
87218
- const courseById = new Map;
87219
- for (const course of courses) {
87220
- const courseId = course.ids?.[apiEnv];
87221
- if (courseId) {
87222
- courseById.set(courseId, course);
87223
- }
87224
- }
87225
- return courseById;
87226
- }
87227
- function mapEnrollmentsToCourses(enrollments, courseById) {
87228
- return enrollments.map((enrollment) => {
87229
- const configuredCourse = courseById.get(enrollment.course.id);
87230
- return {
87231
- id: enrollment.course.id,
87232
- code: configuredCourse?.courseCode ?? enrollment.course.id,
87233
- name: enrollment.course.title
87234
- };
87235
- });
87236
- }
87237
- function pickGoalsFromEnrollments(enrollments) {
87238
- return enrollments.map((enrollment) => enrollment.metadata?.goals).find(Boolean);
87239
- }
87240
- function getUtcDayRange(date6) {
87241
- const start = new Date(Date.UTC(date6.getUTCFullYear(), date6.getUTCMonth(), date6.getUTCDate()));
87242
- const end = new Date(Date.UTC(date6.getUTCFullYear(), date6.getUTCMonth(), date6.getUTCDate(), 23, 59, 59, 999));
87243
- return { start, end };
87244
- }
87245
- function sumXp(facts) {
87246
- return Object.values(facts).reduce((dateTotal, subjects) => {
87247
- return dateTotal + Object.values(subjects).reduce((subjectTotal, metrics) => {
87248
- return subjectTotal + (metrics.activityMetrics?.xpEarned ?? 0);
87249
- }, 0);
87250
- }, 0);
87251
- }
87252
- // src/server/handlers/identity-full.ts
87296
+ // src/server/lib/sso.ts
87253
87297
  function buildErrorContext(error57, errorCode, state, req) {
87254
87298
  return {
87255
87299
  error: error57,
@@ -87268,123 +87312,59 @@ function tryDecodeState(stateParam) {
87268
87312
  return;
87269
87313
  }
87270
87314
  }
87271
- function handleCallbackError(errorParam, url6, state, req, identity) {
87315
+ function handleIdpError(errorParam, url6, state, req, onCallbackError) {
87272
87316
  const errorDesc = url6.searchParams.get("error_description");
87273
87317
  ssoLog.error("IdP returned error", { error: errorParam, description: errorDesc });
87274
87318
  const error57 = new Error(errorDesc ?? errorParam);
87275
- if (identity.onCallbackError) {
87276
- return identity.onCallbackError(buildErrorContext(error57, errorParam, state, req));
87319
+ if (onCallbackError) {
87320
+ return onCallbackError(buildErrorContext(error57, errorParam, state, req));
87277
87321
  }
87278
87322
  return jsonResponse({ error: errorParam }, 400);
87279
87323
  }
87280
- function handleMissingCode(state, req, identity) {
87324
+ function handleMissingCode(state, req, onCallbackError) {
87281
87325
  ssoLog.error("Missing authorization code in callback");
87282
87326
  const error57 = new Error("Missing authorization code");
87283
- if (identity.onCallbackError) {
87284
- return identity.onCallbackError(buildErrorContext(error57, "missing_code", state, req));
87327
+ if (onCallbackError) {
87328
+ return onCallbackError(buildErrorContext(error57, "missing_code", state, req));
87285
87329
  }
87286
87330
  return jsonResponse({ error: "Missing authorization code" }, 400);
87287
87331
  }
87288
- async function handleSignIn(req, env2, identity) {
87289
- if (identity.mode !== "sso") {
87290
- ssoLog.warn("SSO not configured");
87291
- return jsonResponse({ error: "SSO not configured" }, 400);
87292
- }
87293
- const issuer = identity.issuer ?? getIssuer(env2);
87332
+ async function initiateSignIn(params) {
87333
+ const { req, env: env2, clientId, buildState } = params;
87334
+ const issuer = params.issuer ?? getIssuer(env2);
87294
87335
  const url6 = new URL(req.url);
87295
- let redirectUri = identity.redirectUri;
87336
+ let redirectUri = params.redirectUri;
87296
87337
  if (!redirectUri) {
87297
87338
  const basePath = url6.pathname.replace(ROUTES.IDENTITY.SIGNIN, "");
87298
87339
  redirectUri = `${url6.origin}${basePath}${ROUTES.IDENTITY.CALLBACK}`;
87299
87340
  }
87300
- ssoLog.debug("SSO sign-in initiated", { env: env2, issuer, clientId: identity.clientId, redirectUri });
87301
- const stateData = identity.buildState ? identity.buildState({ req, url: url6 }) : {};
87341
+ ssoLog.debug("SSO sign-in initiated", { env: env2, issuer, clientId, redirectUri });
87342
+ const stateData = buildState ? buildState({ req, url: url6 }) : {};
87302
87343
  const state = encodeBase64Url(stateData);
87303
87344
  const authUrl = await buildAuthorizationUrl({
87304
87345
  issuer,
87305
- clientId: identity.clientId,
87346
+ clientId,
87306
87347
  redirectUri,
87307
87348
  state
87308
87349
  });
87309
87350
  return redirectResponse(authUrl);
87310
87351
  }
87311
- async function completeCallback(code, url6, state, req, env2, identity, api) {
87312
- try {
87313
- const issuer = identity.issuer ?? getIssuer(env2);
87314
- let redirectUri = identity.redirectUri;
87315
- if (!redirectUri) {
87316
- const basePath = url6.pathname.replace(ROUTES.IDENTITY.CALLBACK, "");
87317
- redirectUri = `${url6.origin}${basePath}${ROUTES.IDENTITY.CALLBACK}`;
87318
- }
87319
- ssoLog.debug("Exchanging auth code for tokens", { issuer, clientId: identity.clientId });
87320
- const tokens = await exchangeCodeForTokens({
87321
- issuer,
87322
- clientId: identity.clientId,
87323
- clientSecret: identity.clientSecret,
87324
- code,
87325
- redirectUri
87326
- });
87327
- const userInfo = await getUserInfo({ issuer, accessToken: tokens.access_token });
87328
- const identities = typeof userInfo.identities === "string" ? JSON.parse(userInfo.identities) : userInfo.identities;
87329
- ssoLog.debug("SSO completed, resolving Timeback user", {
87330
- user: { ...userInfo, identities }
87331
- });
87332
- const authUser = await resolveTimebackUserByEmail({
87333
- env: env2,
87334
- apiCredentials: api.credentials,
87335
- userInfo,
87336
- client: api.getClient()
87337
- });
87338
- ssoLog.debug("Timeback user resolved", { timebackId: authUser.id });
87339
- const ctx = {
87340
- user: authUser,
87341
- idp: { tokens, userInfo },
87342
- state,
87343
- req,
87344
- redirect: redirectResponse,
87345
- json: jsonResponse
87346
- };
87347
- return identity.onCallbackSuccess(ctx);
87348
- } catch (err) {
87349
- const error57 = err instanceof Error ? err : new Error("Unknown error");
87350
- const errorCode = err instanceof TimebackUserResolutionError ? err.code : "token_exchange_failed";
87351
- ssoLog.error("SSO callback failed", { error: error57.message, errorCode });
87352
- if (identity.onCallbackError) {
87353
- return identity.onCallbackError(buildErrorContext(error57, errorCode, state, req));
87354
- }
87355
- return jsonResponse({ error: error57.message }, 500);
87356
- }
87357
- }
87358
- async function handleCallback(req, env2, identity, api) {
87359
- if (identity.mode !== "sso") {
87360
- ssoLog.warn("SSO not configured");
87361
- return jsonResponse({ error: "SSO not configured" }, 400);
87362
- }
87352
+ function parseCallback(req) {
87363
87353
  const url6 = new URL(req.url);
87364
87354
  const code = url6.searchParams.get("code");
87365
87355
  const errorParam = url6.searchParams.get("error");
87366
87356
  const stateParam = url6.searchParams.get("state");
87367
87357
  ssoLog.debug("Received callback from IdP", { hasCode: !!code, error: errorParam });
87368
87358
  const state = stateParam ? tryDecodeState(stateParam) : undefined;
87369
- if (errorParam) {
87370
- return await handleCallbackError(errorParam, url6, state, req, identity);
87371
- }
87372
- if (!code) {
87373
- return await handleMissingCode(state, req, identity);
87374
- }
87375
- return await completeCallback(code, url6, state, req, env2, identity, api);
87359
+ return { url: url6, code, errorParam, state };
87376
87360
  }
87377
- function createIdentityHandlers(params) {
87378
- const { env: env2, identity, api } = params;
87379
- return {
87380
- signIn: (req) => handleSignIn(req, env2, identity),
87381
- callback: (req) => handleCallback(req, env2, identity, api),
87382
- signOut: () => redirectResponse("/")
87383
- };
87361
+ function computeRedirectUri(url6, configuredRedirectUri) {
87362
+ if (configuredRedirectUri) {
87363
+ return configuredRedirectUri;
87364
+ }
87365
+ const basePath = url6.pathname.replace(ROUTES.IDENTITY.CALLBACK, "");
87366
+ return `${url6.origin}${basePath}${ROUTES.IDENTITY.CALLBACK}`;
87384
87367
  }
87385
- // src/server/handlers/activity.ts
87386
- import * as z16 from "zod";
87387
-
87388
87368
  // ../clients/caliper/dist/index.js
87389
87369
  var __defProp7 = Object.defineProperty;
87390
87370
  var __export = (target, all) => {
@@ -102317,7 +102297,7 @@ var TimebackSubject8 = exports_external6.enum([
102317
102297
  "Math",
102318
102298
  "None",
102319
102299
  "Other"
102320
- ]);
102300
+ ]).meta({ id: "TimebackSubject", description: "Subject area" });
102321
102301
  var TimebackGrade8 = exports_external6.union([
102322
102302
  exports_external6.literal(-1),
102323
102303
  exports_external6.literal(0),
@@ -102334,7 +102314,10 @@ var TimebackGrade8 = exports_external6.union([
102334
102314
  exports_external6.literal(11),
102335
102315
  exports_external6.literal(12),
102336
102316
  exports_external6.literal(13)
102337
- ]);
102317
+ ]).meta({
102318
+ id: "TimebackGrade",
102319
+ description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
102320
+ });
102338
102321
  var ScoreStatus8 = exports_external6.enum([
102339
102322
  "exempt",
102340
102323
  "fully graded",
@@ -102564,62 +102547,84 @@ var CaliperListEventsParams8 = exports_external6.object({
102564
102547
  actorEmail: exports_external6.email().optional()
102565
102548
  }).strict();
102566
102549
  var CourseIds8 = exports_external6.object({
102567
- staging: exports_external6.string().optional(),
102568
- production: exports_external6.string().optional()
102569
- });
102570
- var CourseType8 = exports_external6.enum(["base", "hole-filling", "optional"]);
102571
- var PublishStatus8 = exports_external6.enum(["draft", "testing", "published", "deactivated"]);
102550
+ staging: exports_external6.string().meta({ description: "Course ID in staging environment" }).optional(),
102551
+ production: exports_external6.string().meta({ description: "Course ID in production environment" }).optional()
102552
+ }).meta({ id: "CourseIds", description: "Environment-specific course IDs (populated by sync)" });
102553
+ var CourseType8 = exports_external6.enum(["base", "hole-filling", "optional"]).meta({ id: "CourseType", description: "Course classification type" });
102554
+ var PublishStatus8 = exports_external6.enum(["draft", "testing", "published", "deactivated"]).meta({ id: "PublishStatus", description: "Course publication status" });
102572
102555
  var CourseGoals8 = exports_external6.object({
102573
- dailyXp: exports_external6.number().int().positive().optional(),
102574
- dailyLessons: exports_external6.number().int().positive().optional(),
102575
- dailyActiveMinutes: exports_external6.number().int().positive().optional(),
102576
- dailyAccuracy: exports_external6.number().int().min(0).max(100).optional(),
102577
- dailyMasteredUnits: exports_external6.number().int().positive().optional()
102578
- });
102556
+ dailyXp: exports_external6.number().int().positive().meta({ description: "Target XP to earn per day" }).optional(),
102557
+ dailyLessons: exports_external6.number().int().positive().meta({ description: "Target lessons to complete per day" }).optional(),
102558
+ dailyActiveMinutes: exports_external6.number().int().positive().meta({ description: "Target active learning minutes per day" }).optional(),
102559
+ dailyAccuracy: exports_external6.number().int().min(0).max(100).meta({ description: "Target accuracy percentage (0-100)" }).optional(),
102560
+ dailyMasteredUnits: exports_external6.number().int().positive().meta({ description: "Target units to master per day" }).optional()
102561
+ }).meta({ id: "CourseGoals", description: "Daily learning goals for a course" });
102579
102562
  var CourseMetrics8 = exports_external6.object({
102580
- totalXp: exports_external6.number().int().positive().optional(),
102581
- totalLessons: exports_external6.number().int().positive().optional(),
102582
- totalGrades: exports_external6.number().int().positive().optional()
102583
- });
102563
+ totalXp: exports_external6.number().int().positive().meta({ description: "Total XP available in the course" }).optional(),
102564
+ totalLessons: exports_external6.number().int().positive().meta({ description: "Total number of lessons/activities" }).optional(),
102565
+ totalGrades: exports_external6.number().int().positive().meta({ description: "Total grade levels covered" }).optional()
102566
+ }).meta({ id: "CourseMetrics", description: "Aggregate metrics for a course" });
102584
102567
  var CourseMetadata8 = exports_external6.object({
102585
102568
  courseType: CourseType8.optional(),
102586
- isSupplemental: exports_external6.boolean().optional(),
102587
- isCustom: exports_external6.boolean().optional(),
102569
+ isSupplemental: exports_external6.boolean().meta({ description: "Whether this is supplemental to a base course" }).optional(),
102570
+ isCustom: exports_external6.boolean().meta({ description: "Whether this is a custom course for an individual student" }).optional(),
102588
102571
  publishStatus: PublishStatus8.optional(),
102589
- contactEmail: exports_external6.email().optional(),
102590
- primaryApp: exports_external6.string().optional(),
102572
+ contactEmail: exports_external6.email().meta({ description: "Contact email for course issues" }).optional(),
102573
+ primaryApp: exports_external6.string().meta({ description: "Primary application identifier" }).optional(),
102591
102574
  goals: CourseGoals8.optional(),
102592
102575
  metrics: CourseMetrics8.optional()
102593
- });
102576
+ }).meta({ id: "CourseMetadata", description: "Course metadata (matches API metadata object)" });
102594
102577
  var CourseDefaults8 = exports_external6.object({
102595
- courseCode: exports_external6.string().optional(),
102596
- level: exports_external6.string().optional(),
102578
+ courseCode: exports_external6.string().meta({ description: "Course code (e.g., 'MATH101')" }).optional(),
102579
+ level: exports_external6.string().meta({ description: "Course level (e.g., 'AP', 'Honors')" }).optional(),
102597
102580
  metadata: CourseMetadata8.optional()
102581
+ }).meta({
102582
+ id: "CourseDefaults",
102583
+ description: "Default properties that apply to all courses unless overridden"
102598
102584
  });
102599
102585
  var CourseEnvOverrides8 = exports_external6.object({
102600
- level: exports_external6.string().optional(),
102601
- sensor: exports_external6.string().url().optional(),
102602
- launchUrl: exports_external6.string().url().optional(),
102586
+ level: exports_external6.string().meta({ description: "Course level for this environment" }).optional(),
102587
+ sensor: exports_external6.url().meta({ description: "Caliper sensor endpoint URL for this environment" }).optional(),
102588
+ launchUrl: exports_external6.url().meta({ description: "LTI launch URL for this environment" }).optional(),
102603
102589
  metadata: CourseMetadata8.optional()
102590
+ }).meta({
102591
+ id: "CourseEnvOverrides",
102592
+ description: "Environment-specific course overrides (non-identity fields)"
102604
102593
  });
102605
102594
  var CourseOverrides8 = exports_external6.object({
102606
- staging: CourseEnvOverrides8.optional(),
102607
- production: CourseEnvOverrides8.optional()
102608
- });
102595
+ staging: CourseEnvOverrides8.meta({
102596
+ description: "Overrides for staging environment"
102597
+ }).optional(),
102598
+ production: CourseEnvOverrides8.meta({
102599
+ description: "Overrides for production environment"
102600
+ }).optional()
102601
+ }).meta({ id: "CourseOverrides", description: "Per-environment course overrides" });
102609
102602
  var CourseConfig8 = CourseDefaults8.extend({
102610
- subject: TimebackSubject8,
102611
- grade: TimebackGrade8.optional(),
102603
+ subject: TimebackSubject8.meta({ description: "Subject area for this course" }),
102604
+ grade: TimebackGrade8.meta({
102605
+ description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
102606
+ }).optional(),
102612
102607
  ids: CourseIds8.nullable().optional(),
102613
- sensor: exports_external6.string().url().optional(),
102614
- launchUrl: exports_external6.string().url().optional(),
102608
+ sensor: exports_external6.url().meta({ description: "Caliper sensor endpoint URL for this course" }).optional(),
102609
+ launchUrl: exports_external6.url().meta({ description: "LTI launch URL for this course" }).optional(),
102615
102610
  overrides: CourseOverrides8.optional()
102611
+ }).meta({
102612
+ id: "CourseConfig",
102613
+ description: "Configuration for a single course. Must have either grade or courseCode (or both)."
102616
102614
  });
102617
102615
  var TimebackConfig8 = exports_external6.object({
102618
- name: exports_external6.string().min(1, "App name is required"),
102619
- defaults: CourseDefaults8.optional(),
102620
- courses: exports_external6.array(CourseConfig8).min(1, "At least one course is required"),
102621
- sensor: exports_external6.string().url().optional(),
102622
- launchUrl: exports_external6.string().url().optional()
102616
+ $schema: exports_external6.string().meta({ description: "JSON Schema reference for editor support" }).optional(),
102617
+ name: exports_external6.string().min(1, "App name is required").meta({ description: "Display name for your app" }),
102618
+ defaults: CourseDefaults8.meta({
102619
+ description: "Default properties applied to all courses"
102620
+ }).optional(),
102621
+ courses: exports_external6.array(CourseConfig8).min(1, "At least one course is required").meta({ description: "Courses available in this app" }),
102622
+ sensor: exports_external6.url().meta({ description: "Default Caliper sensor endpoint URL for all courses" }).optional(),
102623
+ launchUrl: exports_external6.url().meta({ description: "Default LTI launch URL for all courses" }).optional()
102624
+ }).meta({
102625
+ id: "TimebackConfig",
102626
+ title: "Timeback Config",
102627
+ description: "Configuration schema for timeback.config.json files"
102623
102628
  }).refine((config22) => {
102624
102629
  return config22.courses.every((c) => c.grade !== undefined || c.courseCode !== undefined);
102625
102630
  }, {
@@ -102640,14 +102645,20 @@ var TimebackConfig8 = exports_external6.object({
102640
102645
  message: "Duplicate courseCode found; each must be unique",
102641
102646
  path: ["courses"]
102642
102647
  }).refine((config22) => {
102643
- return config22.courses.every((c) => c.sensor !== undefined || config22.sensor !== undefined);
102644
- }, {
102645
- message: "Each course must have an effective sensor; set a top-level `sensor` or per-course `sensor`",
102646
- path: ["courses"]
102647
- }).refine((config22) => {
102648
- return config22.courses.every((c) => c.launchUrl !== undefined || config22.launchUrl !== undefined);
102648
+ return config22.courses.every((c) => {
102649
+ if (c.sensor !== undefined || config22.sensor !== undefined) {
102650
+ return true;
102651
+ }
102652
+ const launchUrls = [
102653
+ c.launchUrl,
102654
+ config22.launchUrl,
102655
+ c.overrides?.staging?.launchUrl,
102656
+ c.overrides?.production?.launchUrl
102657
+ ].filter(Boolean);
102658
+ return launchUrls.length > 0;
102659
+ });
102649
102660
  }, {
102650
- message: "Each course must have an effective launchUrl; set a top-level `launchUrl` or per-course `launchUrl`",
102661
+ message: "Each course must have an effective sensor. Either set `sensor` explicitly (top-level or per-course), or provide a `launchUrl` so sensor can be derived from its origin.",
102651
102662
  path: ["courses"]
102652
102663
  });
102653
102664
  var EdubridgeDateString8 = exports_external6.union([IsoDateString8, IsoDateTimeString8]);
@@ -103930,8 +103941,337 @@ function createCaliperClient2(registry22 = DEFAULT_PROVIDER_REGISTRY7) {
103930
103941
  }
103931
103942
  var CaliperClient2 = createCaliperClient2();
103932
103943
 
103933
- // src/server/handlers/activity.ts
103934
- var log11 = createScopedLogger3("handlers:activity");
103944
+ // src/server/handlers/activity/caliper.ts
103945
+ class MissingSyncedCourseIdError extends Error {
103946
+ course;
103947
+ env;
103948
+ constructor(course, env2) {
103949
+ const identifier = course.grade === undefined ? course.courseCode ?? course.subject : `${course.subject} grade ${course.grade}`;
103950
+ super(`Course "${identifier}" is missing a synced ID for ${env2}. Run \`timeback resources push\` first.`);
103951
+ this.name = "MissingSyncedCourseIdError";
103952
+ this.course = course;
103953
+ this.env = env2;
103954
+ }
103955
+ }
103956
+
103957
+ class InvalidSensorUrlError extends Error {
103958
+ sensor;
103959
+ constructor(sensor) {
103960
+ super(`Invalid sensor URL "${sensor}". Sensor must be a valid absolute URL (e.g., "https://sensor.example.com") to support slug-based activity IDs.`);
103961
+ this.name = "InvalidSensorUrlError";
103962
+ this.sensor = sensor;
103963
+ }
103964
+ }
103965
+ function buildCourseId(course, apiEnv) {
103966
+ const courseId = course.ids?.[apiEnv];
103967
+ if (!courseId) {
103968
+ throw new MissingSyncedCourseIdError(course, apiEnv);
103969
+ }
103970
+ return courseId;
103971
+ }
103972
+ function buildCourseName(course) {
103973
+ if (course.courseCode) {
103974
+ return course.courseCode;
103975
+ }
103976
+ if (course.grade !== undefined) {
103977
+ return `${course.subject} G${String(course.grade)}`;
103978
+ }
103979
+ return course.subject;
103980
+ }
103981
+ function buildCanonicalActivityUrl(sensor, selector, slug) {
103982
+ let base;
103983
+ try {
103984
+ base = new URL(sensor);
103985
+ } catch {
103986
+ throw new InvalidSensorUrlError(sensor);
103987
+ }
103988
+ const pathSegment = "grade" in selector ? `${selector.subject}/g${String(selector.grade)}` : selector.code;
103989
+ const basePath = base.pathname.replace(/\/+$/, "");
103990
+ base.pathname = `${basePath}/activities/${pathSegment}/${encodeURIComponent(slug)}`;
103991
+ return base.toString();
103992
+ }
103993
+ function buildActivityContext(payload, course, appName, apiEnv, sensor) {
103994
+ return {
103995
+ id: buildCanonicalActivityUrl(sensor, payload.course, payload.id),
103996
+ type: "TimebackActivityContext",
103997
+ subject: course.subject,
103998
+ app: { name: appName },
103999
+ activity: { name: payload.name },
104000
+ course: {
104001
+ id: buildCourseId(course, apiEnv),
104002
+ name: buildCourseName(course)
104003
+ },
104004
+ process: false
104005
+ };
104006
+ }
104007
+ function buildActivityMetrics(metrics) {
104008
+ const result = [];
104009
+ if (metrics.totalQuestions !== undefined) {
104010
+ result.push({ type: "totalQuestions", value: metrics.totalQuestions });
104011
+ }
104012
+ if (metrics.correctQuestions !== undefined) {
104013
+ result.push({ type: "correctQuestions", value: metrics.correctQuestions });
104014
+ }
104015
+ if (metrics.xpEarned !== undefined) {
104016
+ result.push({ type: "xpEarned", value: metrics.xpEarned });
104017
+ }
104018
+ if (metrics.masteredUnits !== undefined) {
104019
+ result.push({ type: "masteredUnits", value: metrics.masteredUnits });
104020
+ }
104021
+ return result;
104022
+ }
104023
+ function buildTimeSpentMetrics(elapsedMs, pausedMs) {
104024
+ const result = [{ type: "active", value: Math.max(0, elapsedMs) / 1000 }];
104025
+ if (pausedMs > 0) {
104026
+ result.push({ type: "inactive", value: Math.max(0, pausedMs) / 1000 });
104027
+ }
104028
+ return result;
104029
+ }
104030
+ function buildActivityEvents(params) {
104031
+ const { sensor, timebackId, email: email8, payload, course, appName, apiEnv } = params;
104032
+ const actor = {
104033
+ id: `urn:timeback:user:${timebackId}`,
104034
+ type: "TimebackUser",
104035
+ email: email8
104036
+ };
104037
+ const object7 = buildActivityContext(payload, course, appName, apiEnv, sensor);
104038
+ const metrics = buildActivityMetrics(payload.metrics);
104039
+ const timeSpentMetrics = buildTimeSpentMetrics(payload.elapsedMs, payload.pausedMs);
104040
+ const activityEvent = createActivityEvent2({
104041
+ actor,
104042
+ object: object7,
104043
+ metrics,
104044
+ eventTime: payload.endedAt,
104045
+ attempt: payload.attemptNumber,
104046
+ generatedExtensions: payload.pctCompleteApp === undefined ? undefined : { pctCompleteApp: payload.pctCompleteApp }
104047
+ });
104048
+ const timeSpentEvent = createTimeSpentEvent2({
104049
+ actor,
104050
+ object: object7,
104051
+ metrics: timeSpentMetrics,
104052
+ eventTime: payload.endedAt
104053
+ });
104054
+ return {
104055
+ sensor,
104056
+ actor,
104057
+ object: object7,
104058
+ events: [activityEvent, timeSpentEvent],
104059
+ payload,
104060
+ course,
104061
+ appName,
104062
+ apiEnv,
104063
+ email: email8,
104064
+ timebackId
104065
+ };
104066
+ }
104067
+ async function sendCaliperEnvelope(client, sensor, activityEvent, timeSpentEvent) {
104068
+ await client.caliper.events.send(sensor, [activityEvent, timeSpentEvent]);
104069
+ }
104070
+
104071
+ // src/server/handlers/activity/gradebook.ts
104072
+ var log11 = createScopedLogger3("handlers:activity:gradebook");
104073
+ function buildAssessmentResultPayload(params) {
104074
+ const { resultId, lineItemId, timebackId, attempt, write } = params;
104075
+ return {
104076
+ sourcedId: resultId,
104077
+ status: "active",
104078
+ assessmentLineItem: { sourcedId: lineItemId },
104079
+ student: { sourcedId: timebackId },
104080
+ score: write.score,
104081
+ scoreDate: write.endedAt,
104082
+ scoreStatus: "fully graded",
104083
+ inProgress: "false",
104084
+ metadata: {
104085
+ totalQuestions: write.metrics.totalQuestions,
104086
+ correctQuestions: write.metrics.correctQuestions,
104087
+ accuracy: write.score,
104088
+ xpEarned: write.metrics.xpEarned,
104089
+ masteredUnits: write.metrics.masteredUnits,
104090
+ attempt,
104091
+ endedAt: write.endedAt,
104092
+ pctCompleteApp: write.pctCompleteApp,
104093
+ appName: write.appName,
104094
+ lastUpdated: new Date().toISOString()
104095
+ }
104096
+ };
104097
+ }
104098
+ function buildAttemptResultId(lineItemId, timebackId, attempt, endedAt) {
104099
+ return `${lineItemId}:${safeIdSegment(timebackId)}:attempt-${attempt}:e-${hashSuffix64Base36(endedAt)}`;
104100
+ }
104101
+ async function ensureAssessmentLineItemExists(params) {
104102
+ const { client, lineItemId, activityName, courseId, appName } = params;
104103
+ try {
104104
+ await client.oneroster.assessmentLineItems(lineItemId).get();
104105
+ return;
104106
+ } catch {}
104107
+ try {
104108
+ await client.oneroster.assessmentLineItems.create({
104109
+ sourcedId: lineItemId,
104110
+ title: activityName,
104111
+ status: "active",
104112
+ course: { sourcedId: courseId },
104113
+ resultValueMin: 0,
104114
+ resultValueMax: 100,
104115
+ metadata: {
104116
+ createdBy: "timeback-sdk",
104117
+ appName
104118
+ }
104119
+ });
104120
+ } catch (err) {
104121
+ try {
104122
+ await client.oneroster.assessmentLineItems(lineItemId).get();
104123
+ } catch {
104124
+ throw err;
104125
+ }
104126
+ }
104127
+ log11.debug("Created assessment line item", { lineItemId });
104128
+ }
104129
+ function findRetryResult(existingResults, endedAt) {
104130
+ return existingResults.find((result) => {
104131
+ const metadata = result.metadata;
104132
+ return result.scoreDate === endedAt || metadata?.endedAt === endedAt;
104133
+ });
104134
+ }
104135
+ function computeMaxAttempt(existingResults) {
104136
+ let maxAttempt = 0;
104137
+ for (const result of existingResults) {
104138
+ const metadata = result.metadata;
104139
+ const attempt = metadata?.attempt;
104140
+ if (typeof attempt === "number" && attempt > maxAttempt) {
104141
+ maxAttempt = attempt;
104142
+ }
104143
+ }
104144
+ return maxAttempt;
104145
+ }
104146
+ function resolveAttemptFromResult(result) {
104147
+ const metadata = result.metadata;
104148
+ return typeof metadata?.attempt === "number" && metadata.attempt >= 1 ? metadata.attempt : 1;
104149
+ }
104150
+ async function writeAssessmentResultWithAttemptReservation(params) {
104151
+ const { client, lineItemId, timebackId, write } = params;
104152
+ const existingResults = await client.oneroster.assessmentResults.listAll({
104153
+ where: {
104154
+ status: "active",
104155
+ "assessmentLineItem.sourcedId": lineItemId,
104156
+ "student.sourcedId": timebackId
104157
+ }
104158
+ });
104159
+ const retryResult = findRetryResult(existingResults, write.endedAt);
104160
+ if (retryResult) {
104161
+ const attempt2 = resolveAttemptFromResult(retryResult);
104162
+ const resultId2 = retryResult.sourcedId || buildAttemptResultId(lineItemId, timebackId, attempt2, write.endedAt);
104163
+ await client.oneroster.assessmentResults.update(resultId2, buildAssessmentResultPayload({
104164
+ resultId: resultId2,
104165
+ lineItemId,
104166
+ timebackId,
104167
+ attempt: attempt2,
104168
+ write
104169
+ }));
104170
+ log11.debug("Wrote gradebook entry (retry)", {
104171
+ lineItemId,
104172
+ resultId: resultId2,
104173
+ score: write.score,
104174
+ attempt: attempt2
104175
+ });
104176
+ return;
104177
+ }
104178
+ const attempt = computeMaxAttempt(existingResults) + 1;
104179
+ const resultId = buildAttemptResultId(lineItemId, timebackId, attempt, write.endedAt);
104180
+ await client.oneroster.assessmentResults.update(resultId, buildAssessmentResultPayload({
104181
+ resultId,
104182
+ lineItemId,
104183
+ timebackId,
104184
+ attempt,
104185
+ write
104186
+ }));
104187
+ log11.debug("Wrote gradebook entry (new attempt)", {
104188
+ lineItemId,
104189
+ resultId,
104190
+ score: write.score,
104191
+ attempt
104192
+ });
104193
+ }
104194
+ async function writeGradebookEntry(params) {
104195
+ const { client, courseId, activityId, activityName, timebackId, payload, appName } = params;
104196
+ const { metrics, endedAt, pctCompleteApp } = payload;
104197
+ if (metrics.totalQuestions === undefined || metrics.correctQuestions === undefined) {
104198
+ log11.debug("Skipping gradebook write: missing totalQuestions or correctQuestions", {
104199
+ activityId
104200
+ });
104201
+ return;
104202
+ }
104203
+ if (metrics.totalQuestions <= 0) {
104204
+ log11.debug("Skipping gradebook write: totalQuestions must be positive", {
104205
+ activityId,
104206
+ totalQuestions: metrics.totalQuestions
104207
+ });
104208
+ return;
104209
+ }
104210
+ const rawScore = metrics.correctQuestions / metrics.totalQuestions * 100;
104211
+ const score = Math.round(Math.min(100, Math.max(0, rawScore)));
104212
+ const lineItemId = `${safeIdSegment(courseId)}-${safeIdSegment(activityId)}-assessment`;
104213
+ try {
104214
+ await ensureAssessmentLineItemExists({
104215
+ client,
104216
+ lineItemId,
104217
+ activityName,
104218
+ courseId,
104219
+ appName
104220
+ });
104221
+ await writeAssessmentResultWithAttemptReservation({
104222
+ client,
104223
+ lineItemId,
104224
+ timebackId,
104225
+ write: { score, endedAt, metrics, pctCompleteApp, appName }
104226
+ });
104227
+ } catch (err) {
104228
+ const message = err instanceof Error ? err.message : "Unknown error";
104229
+ log11.warn("Failed to write gradebook entry", {
104230
+ lineItemId,
104231
+ error: message
104232
+ });
104233
+ }
104234
+ }
104235
+
104236
+ // src/server/handlers/activity/schema.ts
104237
+ import * as z16 from "zod";
104238
+
104239
+ // src/server/handlers/activity/resolve.ts
104240
+ class ActivityCourseResolutionError extends Error {
104241
+ code;
104242
+ selector;
104243
+ count;
104244
+ constructor(code, selector, count) {
104245
+ super(code);
104246
+ this.code = code;
104247
+ this.selector = selector;
104248
+ this.count = count;
104249
+ }
104250
+ get selectorDescription() {
104251
+ if ("grade" in this.selector) {
104252
+ return `${this.selector.subject} grade ${this.selector.grade}`;
104253
+ }
104254
+ return `code "${this.selector.code}"`;
104255
+ }
104256
+ }
104257
+ function resolveActivityCourse(courses, courseRef) {
104258
+ let matches;
104259
+ if ("grade" in courseRef) {
104260
+ matches = courses.filter((c) => c.subject === courseRef.subject && c.grade === courseRef.grade);
104261
+ } else {
104262
+ matches = courses.filter((c) => c.courseCode === courseRef.code);
104263
+ }
104264
+ if (matches.length === 0) {
104265
+ throw new ActivityCourseResolutionError("unknown_course", courseRef);
104266
+ }
104267
+ if (matches.length > 1) {
104268
+ throw new ActivityCourseResolutionError("ambiguous_course", courseRef, matches.length);
104269
+ }
104270
+ return matches[0];
104271
+ }
104272
+
104273
+ // src/server/handlers/activity/schema.ts
104274
+ var log12 = createScopedLogger3("handlers:activity:schema");
103935
104275
  var activityMetricsSchema = z16.object({
103936
104276
  totalQuestions: z16.number().int().nonnegative().optional(),
103937
104277
  correctQuestions: z16.number().int().nonnegative().optional(),
@@ -103983,16 +104323,16 @@ function validateActivityRequest(body, appConfig, env2) {
103983
104323
  if (err instanceof ActivityCourseResolutionError) {
103984
104324
  const selectorDesc = formatCourseSelector(payload.course);
103985
104325
  if (err.code === "unknown_course") {
103986
- log11.warn("Unknown course selector", { selector: payload.course });
104326
+ log12.warn("Unknown course selector", { selector: payload.course });
103987
104327
  return {
103988
104328
  ok: false,
103989
104329
  response: jsonResponse({ success: false, error: `Unknown course: ${selectorDesc}` }, 400)
103990
104330
  };
103991
104331
  }
103992
- log11.error("Ambiguous course selector", { selector: payload.course });
104332
+ log12.error("Ambiguous course selector", { selector: payload.course });
103993
104333
  return {
103994
104334
  ok: false,
103995
- response: jsonResponse({ success: false, error: "Ambiguous course selector in timeback.config.ts" }, 500)
104335
+ response: jsonResponse({ success: false, error: "Ambiguous course selector in timeback.config.json" }, 500)
103996
104336
  };
103997
104337
  }
103998
104338
  throw err;
@@ -104001,17 +104341,20 @@ function validateActivityRequest(body, appConfig, env2) {
104001
104341
  const sensor = course.overrides?.[envForOverrides]?.sensor ?? course.sensor ?? appConfig.sensor;
104002
104342
  if (!sensor) {
104003
104343
  const selectorDesc = formatCourseSelector(payload.course);
104004
- log11.error("Missing sensor for course", { selector: payload.course });
104344
+ log12.error("Missing sensor for course", { selector: payload.course });
104005
104345
  return {
104006
104346
  ok: false,
104007
104347
  response: jsonResponse({
104008
104348
  success: false,
104009
- error: `Course "${selectorDesc}" has no sensor configured. Set 'courses[].overrides.${envForOverrides}.sensor', 'courses[].sensor', or top-level 'sensor' in timeback.config.ts.`
104349
+ error: `Course "${selectorDesc}" has no sensor configured. Set 'courses[].overrides.${envForOverrides}.sensor', 'courses[].sensor', or top-level 'sensor' in timeback.config.json.`
104010
104350
  }, 500)
104011
104351
  };
104012
104352
  }
104013
104353
  return { ok: true, payload, course, sensor };
104014
104354
  }
104355
+
104356
+ // src/server/handlers/activity/handler.ts
104357
+ var log13 = createScopedLogger3("handlers:activity");
104015
104358
  async function getActivityUserInfo(identity, req) {
104016
104359
  if (identity.mode === "custom") {
104017
104360
  const email8 = await identity.getEmail(req);
@@ -104023,55 +104366,18 @@ async function getActivityUserInfo(identity, req) {
104023
104366
  function resolveTimebackId(userInfo, client) {
104024
104367
  return userInfo.timebackId ?? lookupTimebackIdByEmail({ email: userInfo.email, client });
104025
104368
  }
104026
- function buildActivityEvents(params) {
104027
- const { sensor, timebackId, email: email8, payload, course, appName, apiEnv } = params;
104028
- const actor = {
104029
- id: `urn:timeback:user:${timebackId}`,
104030
- type: "TimebackUser",
104031
- email: email8
104032
- };
104033
- const object7 = buildActivityContext(payload, course, appName, apiEnv, sensor);
104034
- const metrics = buildActivityMetrics(payload.metrics);
104035
- const timeSpentMetrics = buildTimeSpentMetrics(payload.elapsedMs, payload.pausedMs);
104036
- const activityEvent = createActivityEvent2({
104037
- actor,
104038
- object: object7,
104039
- metrics,
104040
- eventTime: payload.endedAt,
104041
- attempt: payload.attemptNumber,
104042
- generatedExtensions: payload.pctCompleteApp === undefined ? undefined : { pctCompleteApp: payload.pctCompleteApp }
104043
- });
104044
- const timeSpentEvent = createTimeSpentEvent2({
104045
- actor,
104046
- object: object7,
104047
- metrics: timeSpentMetrics,
104048
- eventTime: payload.endedAt
104049
- });
104050
- return {
104051
- sensor,
104052
- actor,
104053
- object: object7,
104054
- events: [activityEvent, timeSpentEvent],
104055
- payload,
104056
- course,
104057
- appName,
104058
- apiEnv,
104059
- email: email8,
104060
- timebackId
104061
- };
104062
- }
104063
104369
  function mapErrorToResponse(err, context) {
104064
104370
  if (err instanceof TimebackUserResolutionError) {
104065
- log11.warn("Failed to resolve Timeback user", { code: err.code });
104371
+ log13.warn("Failed to resolve Timeback user", { code: err.code });
104066
104372
  return jsonResponse({ success: false, error: "Unable to resolve Timeback identity" }, resolveStatusForUserResolutionError(err));
104067
104373
  }
104068
104374
  if (err instanceof MissingSyncedCourseIdError) {
104069
104375
  const syncErr = err;
104070
- log11.error("Course not synced", { course: syncErr.course, env: syncErr.env });
104376
+ log13.error("Course not synced", { course: syncErr.course, env: syncErr.env });
104071
104377
  return jsonResponse({ success: false, error: syncErr.message }, 500);
104072
104378
  }
104073
104379
  const message = err instanceof Error ? err.message : "Unknown error";
104074
- log11.error("Failed to submit activity", { ...context, error: message });
104380
+ log13.error("Failed to submit activity", { ...context, error: message });
104075
104381
  return jsonResponse({ success: false, error: message }, 502);
104076
104382
  }
104077
104383
  function createActivityHandler(config7) {
@@ -104117,8 +104423,20 @@ function createActivityHandler(config7) {
104117
104423
  events: effective.events
104118
104424
  });
104119
104425
  }
104426
+ const courseId = effective.object.course?.id;
104427
+ if (courseId) {
104428
+ await writeGradebookEntry({
104429
+ client,
104430
+ courseId,
104431
+ activityId: effective.payload.id,
104432
+ activityName: effective.payload.name,
104433
+ timebackId: effective.timebackId,
104434
+ payload: effective.payload,
104435
+ appName: effective.appName
104436
+ });
104437
+ }
104120
104438
  await sendCaliperEnvelope(client, effective.sensor, effective.events[0], effective.events[1]);
104121
- log11.debug("Submitted activity", {
104439
+ log13.debug("Submitted activity", {
104122
104440
  courseSelector: payload.course,
104123
104441
  activityId: payload.id
104124
104442
  });
@@ -104133,27 +104451,191 @@ function createActivityHandler(config7) {
104133
104451
  }
104134
104452
  };
104135
104453
  }
104136
- // src/server/handlers/user.ts
104137
- var log12 = createScopedLogger3("handlers:user");
104454
+ // src/server/handlers/identity/oidc.ts
104455
+ async function handleSignIn(req, env2, identity) {
104456
+ if (identity.mode !== "sso") {
104457
+ ssoLog.warn("SSO not configured");
104458
+ return jsonResponse({ error: "SSO not configured" }, 400);
104459
+ }
104460
+ return await initiateSignIn({
104461
+ req,
104462
+ env: env2,
104463
+ clientId: identity.clientId,
104464
+ issuer: identity.issuer,
104465
+ redirectUri: identity.redirectUri,
104466
+ buildState: identity.buildState
104467
+ });
104468
+ }
104469
+ async function completeCallback(code, url7, state, req, env2, identity, api) {
104470
+ try {
104471
+ const issuer = identity.issuer ?? getIssuer(env2);
104472
+ const redirectUri = computeRedirectUri(url7, identity.redirectUri);
104473
+ ssoLog.debug("Exchanging auth code for tokens", { issuer, clientId: identity.clientId });
104474
+ const tokens = await exchangeCodeForTokens({
104475
+ issuer,
104476
+ clientId: identity.clientId,
104477
+ clientSecret: identity.clientSecret,
104478
+ code,
104479
+ redirectUri
104480
+ });
104481
+ const userInfo = await getUserInfo({ issuer, accessToken: tokens.access_token });
104482
+ const identities = typeof userInfo.identities === "string" ? JSON.parse(userInfo.identities) : userInfo.identities;
104483
+ ssoLog.debug("SSO completed, resolving Timeback user", {
104484
+ user: { ...userInfo, identities }
104485
+ });
104486
+ const authUser = await resolveTimebackUserByEmail({
104487
+ env: env2,
104488
+ apiCredentials: api.credentials,
104489
+ userInfo,
104490
+ client: api.getClient()
104491
+ });
104492
+ ssoLog.debug("Timeback user resolved", { timebackId: authUser.id });
104493
+ const ctx = {
104494
+ user: authUser,
104495
+ idp: { tokens, userInfo },
104496
+ state,
104497
+ req,
104498
+ redirect: redirectResponse,
104499
+ json: jsonResponse
104500
+ };
104501
+ return identity.onCallbackSuccess(ctx);
104502
+ } catch (err) {
104503
+ const error59 = err instanceof Error ? err : new Error("Unknown error");
104504
+ const errorCode = err instanceof TimebackUserResolutionError ? err.code : "token_exchange_failed";
104505
+ ssoLog.error("SSO callback failed", { error: error59.message, errorCode });
104506
+ if (identity.onCallbackError) {
104507
+ return identity.onCallbackError(buildErrorContext(error59, errorCode, state, req));
104508
+ }
104509
+ return jsonResponse({ error: error59.message }, 500);
104510
+ }
104511
+ }
104512
+ async function handleCallback(req, env2, identity, api) {
104513
+ if (identity.mode !== "sso") {
104514
+ ssoLog.warn("SSO not configured");
104515
+ return jsonResponse({ error: "SSO not configured" }, 400);
104516
+ }
104517
+ const { url: url7, code, errorParam, state } = parseCallback(req);
104518
+ if (errorParam) {
104519
+ return handleIdpError(errorParam, url7, state, req, identity.onCallbackError);
104520
+ }
104521
+ if (!code) {
104522
+ return handleMissingCode(state, req, identity.onCallbackError);
104523
+ }
104524
+ return await completeCallback(code, url7, state, req, env2, identity, api);
104525
+ }
104526
+
104527
+ // src/server/handlers/identity/handler.ts
104528
+ function createIdentityHandlers(params) {
104529
+ const { env: env2, identity, api } = params;
104530
+ return {
104531
+ signIn: (req) => handleSignIn(req, env2, identity),
104532
+ callback: (req) => handleCallback(req, env2, identity, api),
104533
+ signOut: () => redirectResponse("/")
104534
+ };
104535
+ }
104536
+ // src/server/handlers/user/enrollments.ts
104537
+ function buildCourseLookup(courses, apiEnv) {
104538
+ const courseById = new Map;
104539
+ for (const course of courses) {
104540
+ const courseId = course.ids?.[apiEnv];
104541
+ if (courseId) {
104542
+ courseById.set(courseId, course);
104543
+ }
104544
+ }
104545
+ return courseById;
104546
+ }
104547
+ function mapEnrollmentsToCourses(enrollments, courseById) {
104548
+ return enrollments.map((enrollment) => {
104549
+ const configuredCourse = courseById.get(enrollment.course.id);
104550
+ return {
104551
+ id: enrollment.course.id,
104552
+ code: configuredCourse?.courseCode ?? enrollment.course.id,
104553
+ name: enrollment.course.title
104554
+ };
104555
+ });
104556
+ }
104557
+ function pickGoalsFromEnrollments(enrollments) {
104558
+ return enrollments.map((enrollment) => enrollment.metadata?.goals).find(Boolean);
104559
+ }
104560
+ function getUtcDayRange(date10) {
104561
+ const start = new Date(Date.UTC(date10.getUTCFullYear(), date10.getUTCMonth(), date10.getUTCDate()));
104562
+ const end = new Date(Date.UTC(date10.getUTCFullYear(), date10.getUTCMonth(), date10.getUTCDate(), 23, 59, 59, 999));
104563
+ return { start, end };
104564
+ }
104565
+ function sumXp(facts) {
104566
+ return Object.values(facts).reduce((dateTotal, subjects) => {
104567
+ return dateTotal + Object.values(subjects).reduce((subjectTotal, metrics) => {
104568
+ return subjectTotal + (metrics.activityMetrics?.xpEarned ?? 0);
104569
+ }, 0);
104570
+ }, 0);
104571
+ }
104572
+
104573
+ // src/server/handlers/user/profile.ts
104574
+ async function buildUserProfile(client, user, appConfig, apiEnv) {
104575
+ const enrollments = await client.edubridge.enrollments.list({
104576
+ userId: user.id
104577
+ });
104578
+ const courseById = buildCourseLookup(appConfig.courses, apiEnv);
104579
+ const courses = mapEnrollmentsToCourses(enrollments, courseById);
104580
+ const goals = pickGoalsFromEnrollments(enrollments);
104581
+ const { start: todayStart, end: todayEnd } = getUtcDayRange(new Date);
104582
+ const [todayFacts, allFacts] = await Promise.all([
104583
+ client.edubridge.analytics.getActivity({
104584
+ studentId: user.id,
104585
+ startDate: todayStart.toISOString(),
104586
+ endDate: todayEnd.toISOString()
104587
+ }),
104588
+ client.edubridge.analytics.getActivity({
104589
+ studentId: user.id,
104590
+ startDate: "2000-01-01",
104591
+ endDate: todayEnd.toISOString()
104592
+ })
104593
+ ]);
104594
+ return {
104595
+ id: user.id,
104596
+ email: user.email,
104597
+ name: user.name,
104598
+ school: user.school,
104599
+ grade: user.grade,
104600
+ courses: courses.length ? courses : undefined,
104601
+ goals,
104602
+ xp: {
104603
+ today: sumXp(todayFacts),
104604
+ all: sumXp(allFacts)
104605
+ }
104606
+ };
104607
+ }
104608
+
104609
+ // src/server/handlers/user/handler.ts
104610
+ var log14 = createScopedLogger3("handlers:user");
104611
+ async function getUserIdentity(identity, req) {
104612
+ if (identity.mode === "custom") {
104613
+ const email8 = await identity.getEmail(req);
104614
+ if (!email8)
104615
+ return;
104616
+ return { sub: email8, email: email8 };
104617
+ }
104618
+ const user = await identity.getUser(req);
104619
+ if (!user)
104620
+ return;
104621
+ return { sub: user.id, email: user.email };
104622
+ }
104623
+ function mapResolutionErrorToResponse(error59) {
104624
+ log14.warn("Timeback user resolution failed", { code: error59.code });
104625
+ if (error59.code === "timeback_user_ambiguous") {
104626
+ return jsonResponse({ error: "Timeback user resolution ambiguous" }, 409);
104627
+ }
104628
+ if (error59.code === "timeback_user_not_found") {
104629
+ return jsonResponse({ error: "Timeback user not found" }, 404);
104630
+ }
104631
+ return jsonResponse({ error: "User resolution failed" }, 500);
104632
+ }
104138
104633
  function createUserHandler(config7) {
104139
104634
  return async (req) => {
104140
104635
  try {
104141
- let userSub;
104142
- let userEmail;
104143
- if (config7.identity.mode === "custom") {
104144
- const email8 = await config7.identity.getEmail(req);
104145
- if (!email8) {
104146
- return jsonResponse({ error: "Unauthorized" }, 401);
104147
- }
104148
- userSub = email8;
104149
- userEmail = email8;
104150
- } else {
104151
- const user = await config7.identity.getUser(req);
104152
- if (!user) {
104153
- return jsonResponse({ error: "Unauthorized" }, 401);
104154
- }
104155
- userSub = user.id;
104156
- userEmail = user.email;
104636
+ const userIdentity = await getUserIdentity(config7.identity, req);
104637
+ if (!userIdentity) {
104638
+ return jsonResponse({ error: "Unauthorized" }, 401);
104157
104639
  }
104158
104640
  const apiEnv = mapEnvForApi(config7.env);
104159
104641
  const client = new TimebackClient({
@@ -104167,65 +104649,85 @@ function createUserHandler(config7) {
104167
104649
  const resolved = await resolveTimebackUserByEmail({
104168
104650
  env: config7.env,
104169
104651
  apiCredentials: config7.api,
104170
- userInfo: {
104171
- sub: userSub,
104172
- email: userEmail
104173
- },
104652
+ userInfo: userIdentity,
104174
104653
  client
104175
104654
  });
104176
- const enrollments = await client.edubridge.enrollments.list({
104177
- userId: resolved.id
104178
- });
104179
- const courseById = buildCourseLookup(config7.appConfig.courses, apiEnv);
104180
- const courses = mapEnrollmentsToCourses(enrollments, courseById);
104181
- const goals = pickGoalsFromEnrollments(enrollments);
104182
- const { start: todayStart, end: todayEnd } = getUtcDayRange(new Date);
104183
- const [todayFacts, allFacts] = await Promise.all([
104184
- client.edubridge.analytics.getActivity({
104185
- studentId: resolved.id,
104186
- startDate: todayStart.toISOString(),
104187
- endDate: todayEnd.toISOString()
104188
- }),
104189
- client.edubridge.analytics.getActivity({
104190
- studentId: resolved.id,
104191
- startDate: "2000-01-01",
104192
- endDate: todayEnd.toISOString()
104193
- })
104194
- ]);
104195
- const profile = {
104196
- id: resolved.id,
104197
- email: resolved.email,
104198
- name: resolved.name,
104199
- school: resolved.school,
104200
- grade: resolved.grade,
104201
- courses: courses.length ? courses : undefined,
104202
- goals,
104203
- xp: {
104204
- today: sumXp(todayFacts),
104205
- all: sumXp(allFacts)
104206
- }
104207
- };
104655
+ const profile = await buildUserProfile(client, resolved, config7.appConfig, apiEnv);
104208
104656
  return jsonResponse(profile);
104209
104657
  } catch (error59) {
104210
104658
  if (error59 instanceof TimebackUserResolutionError) {
104211
- log12.warn("Timeback user resolution failed", { code: error59.code });
104659
+ return mapResolutionErrorToResponse(error59);
104660
+ }
104661
+ const message = error59 instanceof Error ? error59.message : "Unknown error";
104662
+ log14.error("Failed to build user profile", { error: message });
104663
+ return jsonResponse({ error: message }, 502);
104664
+ } finally {
104665
+ client.close();
104666
+ }
104667
+ } catch (error59) {
104668
+ const message = error59 instanceof Error ? error59.message : "Unknown error";
104669
+ log14.error("Unhandled error in user handler", { error: message });
104670
+ return jsonResponse({ error: message }, 500);
104671
+ }
104672
+ };
104673
+ }
104674
+ // src/server/handlers/user/verify.ts
104675
+ var log15 = createScopedLogger3("handlers:user:verify");
104676
+ async function getUserIdentity2(identity, req) {
104677
+ if (identity.mode === "custom") {
104678
+ const email8 = await identity.getEmail(req);
104679
+ if (!email8)
104680
+ return;
104681
+ return { sub: email8, email: email8 };
104682
+ }
104683
+ const user = await identity.getUser(req);
104684
+ if (!user)
104685
+ return;
104686
+ return { sub: user.id, email: user.email };
104687
+ }
104688
+ function createUserVerifyHandler(config7) {
104689
+ return async (req) => {
104690
+ try {
104691
+ const userIdentity = await getUserIdentity2(config7.identity, req);
104692
+ if (!userIdentity) {
104693
+ return jsonResponse({ verified: false, error: "Unauthorized" }, 401);
104694
+ }
104695
+ const client = new TimebackClient({
104696
+ env: mapEnvForApi(config7.env),
104697
+ auth: {
104698
+ clientId: config7.api.clientId,
104699
+ clientSecret: config7.api.clientSecret
104700
+ }
104701
+ });
104702
+ try {
104703
+ const resolved = await resolveTimebackUserByEmail({
104704
+ env: config7.env,
104705
+ apiCredentials: config7.api,
104706
+ userInfo: userIdentity,
104707
+ client
104708
+ });
104709
+ return jsonResponse({ verified: true, timebackId: resolved.id });
104710
+ } catch (error59) {
104711
+ if (error59 instanceof TimebackUserResolutionError) {
104712
+ log15.warn("Timeback user resolution failed", { code: error59.code });
104212
104713
  if (error59.code === "timeback_user_ambiguous") {
104213
- return jsonResponse({ error: "Timeback user resolution ambiguous" }, 409);
104714
+ return jsonResponse({ verified: false, error: "Timeback user resolution ambiguous" }, 409);
104214
104715
  }
104215
104716
  if (error59.code === "timeback_user_not_found") {
104216
- return jsonResponse({ error: "Timeback user not found" }, 404);
104717
+ return jsonResponse({ verified: false });
104217
104718
  }
104719
+ return jsonResponse({ verified: false, error: error59.message }, 502);
104218
104720
  }
104219
104721
  const message = error59 instanceof Error ? error59.message : "Unknown error";
104220
- log12.error("Failed to build user profile", { error: message });
104221
- return jsonResponse({ error: message }, 502);
104722
+ log15.error("Failed to verify user", { error: message });
104723
+ return jsonResponse({ verified: false, error: message }, 502);
104222
104724
  } finally {
104223
104725
  client.close();
104224
104726
  }
104225
104727
  } catch (error59) {
104226
104728
  const message = error59 instanceof Error ? error59.message : "Unknown error";
104227
- log12.error("Unhandled error in user handler", { error: message });
104228
- return jsonResponse({ error: message }, 500);
104729
+ log15.error("Unhandled error in verify handler", { error: message });
104730
+ return jsonResponse({ verified: false, error: message }, 500);
104229
104731
  }
104230
104732
  };
104231
104733
  }
@@ -104240,6 +104742,7 @@ function toAppCourses(courses) {
104240
104742
  });
104241
104743
  }
104242
104744
  async function createTimeback(config7) {
104745
+ const env2 = normalizeEnv(config7.env);
104243
104746
  const configResult = await loadConfig({ configPath: config7.configPath });
104244
104747
  if (!configResult.success) {
104245
104748
  throw new Error(`Failed to load timeback config: ${configResult.error}`);
@@ -104249,7 +104752,7 @@ async function createTimeback(config7) {
104249
104752
  const getApiClient = () => {
104250
104753
  if (!apiClient) {
104251
104754
  apiClient = new TimebackClient({
104252
- env: mapEnvForApi(config7.env),
104755
+ env: mapEnvForApi(env2),
104253
104756
  auth: {
104254
104757
  clientId: config7.api.clientId,
104255
104758
  clientSecret: config7.api.clientSecret
@@ -104259,7 +104762,7 @@ async function createTimeback(config7) {
104259
104762
  return apiClient;
104260
104763
  };
104261
104764
  const activity = createActivityHandler({
104262
- env: config7.env,
104765
+ env: env2,
104263
104766
  identity: config7.identity,
104264
104767
  appConfig: {
104265
104768
  name: appConfig.name,
@@ -104270,7 +104773,7 @@ async function createTimeback(config7) {
104270
104773
  hooks: config7.hooks
104271
104774
  });
104272
104775
  const identity = createIdentityHandlers({
104273
- env: config7.env,
104776
+ env: env2,
104274
104777
  identity: config7.identity,
104275
104778
  api: {
104276
104779
  credentials: config7.api,
@@ -104278,7 +104781,7 @@ async function createTimeback(config7) {
104278
104781
  }
104279
104782
  });
104280
104783
  const user = createUserHandler({
104281
- env: config7.env,
104784
+ env: env2,
104282
104785
  identity: config7.identity,
104283
104786
  api: config7.api,
104284
104787
  appConfig: {
@@ -104287,13 +104790,19 @@ async function createTimeback(config7) {
104287
104790
  courses: toAppCourses(appConfig.courses)
104288
104791
  }
104289
104792
  });
104793
+ const userVerify = createUserVerifyHandler({
104794
+ env: env2,
104795
+ identity: config7.identity,
104796
+ api: config7.api
104797
+ });
104290
104798
  const instance = {
104291
104799
  config: config7,
104292
104800
  handle: {
104293
104801
  activity,
104294
104802
  identity,
104295
104803
  user: {
104296
- me: user
104804
+ me: user,
104805
+ verify: userVerify
104297
104806
  }
104298
104807
  },
104299
104808
  get api() {
@@ -104302,74 +104811,21 @@ async function createTimeback(config7) {
104302
104811
  };
104303
104812
  return instance;
104304
104813
  }
104305
- // src/server/handlers/identity-only.ts
104306
- function buildErrorContext2(error59, errorCode, state, req) {
104307
- return {
104308
- error: error59,
104309
- errorCode,
104310
- state,
104311
- req,
104312
- redirect: redirectResponse,
104313
- json: jsonResponse
104314
- };
104315
- }
104316
- function tryDecodeState2(stateParam) {
104317
- try {
104318
- return decodeBase64Url(stateParam);
104319
- } catch {
104320
- ssoLog.warn("Failed to decode state");
104321
- return;
104322
- }
104323
- }
104324
- function handleCallbackError2(errorParam, url7, state, req, identity) {
104325
- const errorDesc = url7.searchParams.get("error_description");
104326
- ssoLog.error("IdP returned error", { error: errorParam, description: errorDesc });
104327
- const error59 = new Error(errorDesc ?? errorParam);
104328
- if (identity.onCallbackError) {
104329
- return identity.onCallbackError(buildErrorContext2(error59, errorParam, state, req));
104330
- }
104331
- return jsonResponse({ error: errorParam }, 400);
104332
- }
104333
- function handleMissingCode2(state, req, identity) {
104334
- ssoLog.error("Missing authorization code in callback");
104335
- const error59 = new Error("Missing authorization code");
104336
- if (identity.onCallbackError) {
104337
- return identity.onCallbackError(buildErrorContext2(error59, "missing_code", state, req));
104338
- }
104339
- return jsonResponse({ error: "Missing authorization code" }, 400);
104340
- }
104814
+ // src/server/handlers/identity-only/oidc.ts
104341
104815
  async function handleSignIn2(req, env2, identity) {
104342
- const issuer = identity.issuer ?? getIssuer(env2);
104343
- const url7 = new URL(req.url);
104344
- let redirectUri = identity.redirectUri;
104345
- if (!redirectUri) {
104346
- const basePath = url7.pathname.replace(ROUTES.IDENTITY.SIGNIN, "");
104347
- redirectUri = `${url7.origin}${basePath}${ROUTES.IDENTITY.CALLBACK}`;
104348
- }
104349
- ssoLog.debug("SSO sign-in initiated (identity-only)", {
104816
+ return await initiateSignIn({
104817
+ req,
104350
104818
  env: env2,
104351
- issuer,
104352
- clientId: identity.clientId,
104353
- redirectUri
104354
- });
104355
- const stateData = identity.buildState ? identity.buildState({ req, url: url7 }) : {};
104356
- const state = encodeBase64Url(stateData);
104357
- const authUrl = await buildAuthorizationUrl({
104358
- issuer,
104359
104819
  clientId: identity.clientId,
104360
- redirectUri,
104361
- state
104820
+ issuer: identity.issuer,
104821
+ redirectUri: identity.redirectUri,
104822
+ buildState: identity.buildState
104362
104823
  });
104363
- return redirectResponse(authUrl);
104364
104824
  }
104365
- async function completeCallbackIdentityOnly(code, url7, state, req, env2, identity) {
104825
+ async function completeCallback2(code, url7, state, req, env2, identity) {
104366
104826
  try {
104367
104827
  const issuer = identity.issuer ?? getIssuer(env2);
104368
- let redirectUri = identity.redirectUri;
104369
- if (!redirectUri) {
104370
- const basePath = url7.pathname.replace(ROUTES.IDENTITY.CALLBACK, "");
104371
- redirectUri = `${url7.origin}${basePath}${ROUTES.IDENTITY.CALLBACK}`;
104372
- }
104828
+ const redirectUri = computeRedirectUri(url7, identity.redirectUri);
104373
104829
  ssoLog.debug("Exchanging auth code for tokens (identity-only)", {
104374
104830
  issuer,
104375
104831
  clientId: identity.clientId
@@ -104397,29 +104853,23 @@ async function completeCallbackIdentityOnly(code, url7, state, req, env2, identi
104397
104853
  const error59 = err instanceof Error ? err : new Error("Unknown error");
104398
104854
  ssoLog.error("Token exchange failed (identity-only)", { error: error59.message });
104399
104855
  if (identity.onCallbackError) {
104400
- return identity.onCallbackError(buildErrorContext2(error59, undefined, state, req));
104856
+ return identity.onCallbackError(buildErrorContext(error59, undefined, state, req));
104401
104857
  }
104402
104858
  return jsonResponse({ error: error59.message }, 500);
104403
104859
  }
104404
104860
  }
104405
- async function handleCallbackIdentityOnly(req, env2, identity) {
104406
- const url7 = new URL(req.url);
104407
- const code = url7.searchParams.get("code");
104408
- const errorParam = url7.searchParams.get("error");
104409
- const stateParam = url7.searchParams.get("state");
104410
- ssoLog.debug("Received callback from IdP (identity-only)", {
104411
- hasCode: !!code,
104412
- error: errorParam
104413
- });
104414
- const state = stateParam ? tryDecodeState2(stateParam) : undefined;
104861
+ async function handleCallback2(req, env2, identity) {
104862
+ const { url: url7, code, errorParam, state } = parseCallback(req);
104415
104863
  if (errorParam) {
104416
- return await handleCallbackError2(errorParam, url7, state, req, identity);
104864
+ return handleIdpError(errorParam, url7, state, req, identity.onCallbackError);
104417
104865
  }
104418
104866
  if (!code) {
104419
- return await handleMissingCode2(state, req, identity);
104867
+ return handleMissingCode(state, req, identity.onCallbackError);
104420
104868
  }
104421
- return await completeCallbackIdentityOnly(code, url7, state, req, env2, identity);
104869
+ return await completeCallback2(code, url7, state, req, env2, identity);
104422
104870
  }
104871
+
104872
+ // src/server/handlers/identity-only/handler.ts
104423
104873
  function createIdentityOnlyHandlers(params) {
104424
104874
  const { env: env2, identity } = params;
104425
104875
  if (identity.mode !== "sso") {
@@ -104427,15 +104877,14 @@ function createIdentityOnlyHandlers(params) {
104427
104877
  }
104428
104878
  return {
104429
104879
  signIn: (req) => handleSignIn2(req, env2, identity),
104430
- callback: (req) => handleCallbackIdentityOnly(req, env2, identity),
104880
+ callback: (req) => handleCallback2(req, env2, identity),
104431
104881
  signOut: () => redirectResponse("/")
104432
104882
  };
104433
104883
  }
104434
-
104435
104884
  // src/server/timeback-identity.ts
104436
104885
  function createTimebackIdentity(config7) {
104437
104886
  const identity = createIdentityOnlyHandlers({
104438
- env: config7.env,
104887
+ env: normalizeEnv(config7.env),
104439
104888
  identity: config7.identity
104440
104889
  });
104441
104890
  return {