@timeback/sdk 0.1.6 → 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 (117) hide show
  1. package/dist/client/adapters/react/hooks/types.d.ts +15 -0
  2. package/dist/client/adapters/react/hooks/types.d.ts.map +1 -0
  3. package/dist/client/adapters/react/hooks/useTimebackVerification.d.ts +18 -0
  4. package/dist/client/adapters/react/hooks/useTimebackVerification.d.ts.map +1 -0
  5. package/dist/client/adapters/react/index.d.ts +2 -0
  6. package/dist/client/adapters/react/index.d.ts.map +1 -1
  7. package/dist/client/adapters/react/index.js +139 -9
  8. package/dist/client/auth/bearer.d.ts +17 -0
  9. package/dist/client/auth/bearer.d.ts.map +1 -0
  10. package/dist/client/auth/index.d.ts +3 -0
  11. package/dist/client/auth/index.d.ts.map +1 -0
  12. package/dist/client/auth/types.d.ts +39 -0
  13. package/dist/client/auth/types.d.ts.map +1 -0
  14. package/dist/client/index.d.ts +2 -0
  15. package/dist/client/index.d.ts.map +1 -1
  16. package/dist/client/lib/fetch.d.ts +19 -0
  17. package/dist/client/lib/fetch.d.ts.map +1 -0
  18. package/dist/client/namespaces/user.d.ts +25 -2
  19. package/dist/client/namespaces/user.d.ts.map +1 -1
  20. package/dist/client/timeback-client.class.d.ts +15 -0
  21. package/dist/client/timeback-client.class.d.ts.map +1 -1
  22. package/dist/client/timeback-client.d.ts +3 -0
  23. package/dist/client/timeback-client.d.ts.map +1 -1
  24. package/dist/client.d.ts +2 -1
  25. package/dist/client.d.ts.map +1 -1
  26. package/dist/client.js +69 -6
  27. package/dist/edge.js +85291 -169
  28. package/dist/identity.js +85186 -74
  29. package/dist/index.js +773 -493
  30. package/dist/server/adapters/express.d.ts.map +1 -1
  31. package/dist/server/adapters/express.js +169 -193
  32. package/dist/server/adapters/native.d.ts.map +1 -1
  33. package/dist/server/adapters/native.js +32 -1
  34. package/dist/server/adapters/nextjs.js +32 -1
  35. package/dist/server/adapters/nuxt.d.ts.map +1 -1
  36. package/dist/server/adapters/nuxt.js +173 -193
  37. package/dist/server/adapters/solid-start.d.ts.map +1 -1
  38. package/dist/server/adapters/solid-start.js +173 -193
  39. package/dist/server/adapters/svelte-kit.d.ts.map +1 -1
  40. package/dist/server/adapters/svelte-kit.js +37 -1
  41. package/dist/server/adapters/tanstack-start.d.ts.map +1 -1
  42. package/dist/server/adapters/tanstack-start.js +168 -193
  43. package/dist/server/adapters/utils.d.ts +1 -1
  44. package/dist/server/adapters/utils.d.ts.map +1 -1
  45. package/dist/server/{lib/build-activity-events.d.ts → handlers/activity/caliper.d.ts} +29 -4
  46. package/dist/server/handlers/activity/caliper.d.ts.map +1 -0
  47. package/dist/server/handlers/activity/gradebook.d.ts +56 -0
  48. package/dist/server/handlers/activity/gradebook.d.ts.map +1 -0
  49. package/dist/server/handlers/activity/handler.d.ts +15 -0
  50. package/dist/server/handlers/activity/handler.d.ts.map +1 -0
  51. package/dist/server/handlers/activity/index.d.ts +9 -0
  52. package/dist/server/handlers/activity/index.d.ts.map +1 -0
  53. package/dist/server/handlers/activity/resolve.d.ts +39 -0
  54. package/dist/server/handlers/activity/resolve.d.ts.map +1 -0
  55. package/dist/server/handlers/activity/schema.d.ts +52 -0
  56. package/dist/server/handlers/activity/schema.d.ts.map +1 -0
  57. package/dist/server/handlers/activity/types.d.ts +52 -0
  58. package/dist/server/handlers/activity/types.d.ts.map +1 -0
  59. package/dist/server/handlers/identity/handler.d.ts +14 -0
  60. package/dist/server/handlers/identity/handler.d.ts.map +1 -0
  61. package/dist/server/handlers/identity/index.d.ts +8 -0
  62. package/dist/server/handlers/identity/index.d.ts.map +1 -0
  63. package/dist/server/handlers/identity/oidc.d.ts +43 -0
  64. package/dist/server/handlers/identity/oidc.d.ts.map +1 -0
  65. package/dist/server/handlers/identity/types.d.ts +24 -0
  66. package/dist/server/handlers/identity/types.d.ts.map +1 -0
  67. package/dist/server/handlers/identity-only/handler.d.ts +15 -0
  68. package/dist/server/handlers/identity-only/handler.d.ts.map +1 -0
  69. package/dist/server/handlers/identity-only/index.d.ts +8 -0
  70. package/dist/server/handlers/identity-only/index.d.ts.map +1 -0
  71. package/dist/server/handlers/identity-only/oidc.d.ts +26 -0
  72. package/dist/server/handlers/identity-only/oidc.d.ts.map +1 -0
  73. package/dist/server/handlers/identity-only/types.d.ts +19 -0
  74. package/dist/server/handlers/identity-only/types.d.ts.map +1 -0
  75. package/dist/server/handlers/index.d.ts +5 -2
  76. package/dist/server/handlers/index.d.ts.map +1 -1
  77. package/dist/server/{lib/build-user-profile.d.ts → handlers/user/enrollments.d.ts} +7 -2
  78. package/dist/server/handlers/user/enrollments.d.ts.map +1 -0
  79. package/dist/server/handlers/user/handler.d.ts +17 -0
  80. package/dist/server/handlers/user/handler.d.ts.map +1 -0
  81. package/dist/server/handlers/user/index.d.ts +10 -0
  82. package/dist/server/handlers/user/index.d.ts.map +1 -0
  83. package/dist/server/handlers/user/profile.d.ts +22 -0
  84. package/dist/server/handlers/user/profile.d.ts.map +1 -0
  85. package/dist/server/handlers/user/types.d.ts +35 -0
  86. package/dist/server/handlers/user/types.d.ts.map +1 -0
  87. package/dist/server/handlers/user/verify.d.ts +25 -0
  88. package/dist/server/handlers/user/verify.d.ts.map +1 -0
  89. package/dist/server/index.d.ts +1 -1
  90. package/dist/server/index.d.ts.map +1 -1
  91. package/dist/server/lib/index.d.ts +4 -5
  92. package/dist/server/lib/index.d.ts.map +1 -1
  93. package/dist/server/lib/resolve.d.ts +4 -42
  94. package/dist/server/lib/resolve.d.ts.map +1 -1
  95. package/dist/server/lib/sso.d.ts +86 -0
  96. package/dist/server/lib/sso.d.ts.map +1 -0
  97. package/dist/server/lib/utils.d.ts +32 -1
  98. package/dist/server/lib/utils.d.ts.map +1 -1
  99. package/dist/server/timeback-identity.d.ts.map +1 -1
  100. package/dist/server/timeback.d.ts.map +1 -1
  101. package/dist/server/types.d.ts +16 -9
  102. package/dist/server/types.d.ts.map +1 -1
  103. package/dist/shared/constants.d.ts +1 -0
  104. package/dist/shared/constants.d.ts.map +1 -1
  105. package/dist/shared/types.d.ts +15 -0
  106. package/dist/shared/types.d.ts.map +1 -1
  107. package/package.json +6 -2
  108. package/dist/server/handlers/activity.d.ts +0 -25
  109. package/dist/server/handlers/activity.d.ts.map +0 -1
  110. package/dist/server/handlers/identity-full.d.ts +0 -28
  111. package/dist/server/handlers/identity-full.d.ts.map +0 -1
  112. package/dist/server/handlers/identity-only.d.ts +0 -22
  113. package/dist/server/handlers/identity-only.d.ts.map +0 -1
  114. package/dist/server/handlers/user.d.ts +0 -31
  115. package/dist/server/handlers/user.d.ts.map +0 -1
  116. package/dist/server/lib/build-activity-events.d.ts.map +0 -1
  117. package/dist/server/lib/build-user-profile.d.ts.map +0 -1
@@ -434,12 +434,32 @@ async function getUserInfo(params) {
434
434
  return response.json();
435
435
  }
436
436
  // src/server/lib/utils.ts
437
+ function safeIdSegment(value) {
438
+ return encodeURIComponent(value).replace(/%/g, "_");
439
+ }
440
+ function hashSuffix64Base36(value) {
441
+ let hash = 0xcbf29ce484222325n;
442
+ const prime = 0x100000001b3n;
443
+ const mod64 = 0xffffffffffffffffn;
444
+ for (let i = 0;i < value.length; i++) {
445
+ hash ^= BigInt(value.charCodeAt(i));
446
+ hash = hash * prime & mod64;
447
+ }
448
+ const base36 = hash.toString(36);
449
+ return base36.length > 12 ? base36.slice(-12) : base36;
450
+ }
437
451
  function mapEnvForApi(env) {
438
452
  if (env === "local" || env === "staging") {
439
453
  return "staging";
440
454
  }
441
455
  return "production";
442
456
  }
457
+ function normalizeEnv(env) {
458
+ if (env === "production" || env === "local" || env === "staging") {
459
+ return env;
460
+ }
461
+ return "staging";
462
+ }
443
463
  function jsonResponse(data, status = 200, headers) {
444
464
  const responseHeaders = new Headers(headers);
445
465
  responseHeaders.set("Content-Type", "application/json");
@@ -15739,14 +15759,20 @@ var TimebackConfig = exports_external.object({
15739
15759
  message: "Duplicate courseCode found; each must be unique",
15740
15760
  path: ["courses"]
15741
15761
  }).refine((config2) => {
15742
- return config2.courses.every((c) => c.sensor !== undefined || config2.sensor !== undefined);
15743
- }, {
15744
- message: "Each course must have an effective sensor; set a top-level `sensor` or per-course `sensor`",
15745
- path: ["courses"]
15746
- }).refine((config2) => {
15747
- return config2.courses.every((c) => c.launchUrl !== undefined || config2.launchUrl !== undefined);
15762
+ return config2.courses.every((c) => {
15763
+ if (c.sensor !== undefined || config2.sensor !== undefined) {
15764
+ return true;
15765
+ }
15766
+ const launchUrls = [
15767
+ c.launchUrl,
15768
+ config2.launchUrl,
15769
+ c.overrides?.staging?.launchUrl,
15770
+ c.overrides?.production?.launchUrl
15771
+ ].filter(Boolean);
15772
+ return launchUrls.length > 0;
15773
+ });
15748
15774
  }, {
15749
- message: "Each course must have an effective launchUrl; set a top-level `launchUrl` or per-course `launchUrl`",
15775
+ 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.",
15750
15776
  path: ["courses"]
15751
15777
  });
15752
15778
  var EdubridgeDateString = exports_external.union([IsoDateString, IsoDateTimeString]);
@@ -32142,14 +32168,20 @@ var TimebackConfig2 = exports_external2.object({
32142
32168
  message: "Duplicate courseCode found; each must be unique",
32143
32169
  path: ["courses"]
32144
32170
  }).refine((config22) => {
32145
- return config22.courses.every((c) => c.sensor !== undefined || config22.sensor !== undefined);
32146
- }, {
32147
- message: "Each course must have an effective sensor; set a top-level `sensor` or per-course `sensor`",
32148
- path: ["courses"]
32149
- }).refine((config22) => {
32150
- return config22.courses.every((c) => c.launchUrl !== undefined || config22.launchUrl !== undefined);
32171
+ return config22.courses.every((c) => {
32172
+ if (c.sensor !== undefined || config22.sensor !== undefined) {
32173
+ return true;
32174
+ }
32175
+ const launchUrls = [
32176
+ c.launchUrl,
32177
+ config22.launchUrl,
32178
+ c.overrides?.staging?.launchUrl,
32179
+ c.overrides?.production?.launchUrl
32180
+ ].filter(Boolean);
32181
+ return launchUrls.length > 0;
32182
+ });
32151
32183
  }, {
32152
- message: "Each course must have an effective launchUrl; set a top-level `launchUrl` or per-course `launchUrl`",
32184
+ 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.",
32153
32185
  path: ["courses"]
32154
32186
  });
32155
32187
  var EdubridgeDateString2 = exports_external2.union([IsoDateString2, IsoDateTimeString2]);
@@ -49575,14 +49607,20 @@ var TimebackConfig3 = exports_external3.object({
49575
49607
  message: "Duplicate courseCode found; each must be unique",
49576
49608
  path: ["courses"]
49577
49609
  }).refine((config22) => {
49578
- return config22.courses.every((c) => c.sensor !== undefined || config22.sensor !== undefined);
49579
- }, {
49580
- message: "Each course must have an effective sensor; set a top-level `sensor` or per-course `sensor`",
49581
- path: ["courses"]
49582
- }).refine((config22) => {
49583
- return config22.courses.every((c) => c.launchUrl !== undefined || config22.launchUrl !== undefined);
49610
+ return config22.courses.every((c) => {
49611
+ if (c.sensor !== undefined || config22.sensor !== undefined) {
49612
+ return true;
49613
+ }
49614
+ const launchUrls = [
49615
+ c.launchUrl,
49616
+ config22.launchUrl,
49617
+ c.overrides?.staging?.launchUrl,
49618
+ c.overrides?.production?.launchUrl
49619
+ ].filter(Boolean);
49620
+ return launchUrls.length > 0;
49621
+ });
49584
49622
  }, {
49585
- message: "Each course must have an effective launchUrl; set a top-level `launchUrl` or per-course `launchUrl`",
49623
+ 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.",
49586
49624
  path: ["courses"]
49587
49625
  });
49588
49626
  var EdubridgeDateString3 = exports_external3.union([IsoDateString3, IsoDateTimeString3]);
@@ -67207,14 +67245,20 @@ var TimebackConfig4 = exports_external4.object({
67207
67245
  message: "Duplicate courseCode found; each must be unique",
67208
67246
  path: ["courses"]
67209
67247
  }).refine((config22) => {
67210
- return config22.courses.every((c) => c.sensor !== undefined || config22.sensor !== undefined);
67211
- }, {
67212
- message: "Each course must have an effective sensor; set a top-level `sensor` or per-course `sensor`",
67213
- path: ["courses"]
67214
- }).refine((config22) => {
67215
- return config22.courses.every((c) => c.launchUrl !== undefined || config22.launchUrl !== undefined);
67248
+ return config22.courses.every((c) => {
67249
+ if (c.sensor !== undefined || config22.sensor !== undefined) {
67250
+ return true;
67251
+ }
67252
+ const launchUrls = [
67253
+ c.launchUrl,
67254
+ config22.launchUrl,
67255
+ c.overrides?.staging?.launchUrl,
67256
+ c.overrides?.production?.launchUrl
67257
+ ].filter(Boolean);
67258
+ return launchUrls.length > 0;
67259
+ });
67216
67260
  }, {
67217
- message: "Each course must have an effective launchUrl; set a top-level `launchUrl` or per-course `launchUrl`",
67261
+ 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.",
67218
67262
  path: ["courses"]
67219
67263
  });
67220
67264
  var EdubridgeDateString4 = exports_external4.union([IsoDateString4, IsoDateTimeString4]);
@@ -83869,14 +83913,20 @@ var TimebackConfig5 = exports_external5.object({
83869
83913
  message: "Duplicate courseCode found; each must be unique",
83870
83914
  path: ["courses"]
83871
83915
  }).refine((config22) => {
83872
- return config22.courses.every((c) => c.sensor !== undefined || config22.sensor !== undefined);
83873
- }, {
83874
- message: "Each course must have an effective sensor; set a top-level `sensor` or per-course `sensor`",
83875
- path: ["courses"]
83876
- }).refine((config22) => {
83877
- return config22.courses.every((c) => c.launchUrl !== undefined || config22.launchUrl !== undefined);
83916
+ return config22.courses.every((c) => {
83917
+ if (c.sensor !== undefined || config22.sensor !== undefined) {
83918
+ return true;
83919
+ }
83920
+ const launchUrls = [
83921
+ c.launchUrl,
83922
+ config22.launchUrl,
83923
+ c.overrides?.staging?.launchUrl,
83924
+ c.overrides?.production?.launchUrl
83925
+ ].filter(Boolean);
83926
+ return launchUrls.length > 0;
83927
+ });
83878
83928
  }, {
83879
- message: "Each course must have an effective launchUrl; set a top-level `launchUrl` or per-course `launchUrl`",
83929
+ 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.",
83880
83930
  path: ["courses"]
83881
83931
  });
83882
83932
  var EdubridgeDateString5 = exports_external5.union([IsoDateString5, IsoDateTimeString5]);
@@ -85634,163 +85684,6 @@ async function lookupTimebackIdByEmail(params) {
85634
85684
  throw new TimebackUserResolutionError(`Failed to lookup Timeback user: ${message}`, "timeback_user_lookup_failed");
85635
85685
  }
85636
85686
  }
85637
-
85638
- class ActivityCourseResolutionError extends Error {
85639
- code;
85640
- selector;
85641
- count;
85642
- constructor(code, selector, count) {
85643
- super(code);
85644
- this.code = code;
85645
- this.selector = selector;
85646
- this.count = count;
85647
- }
85648
- get selectorDescription() {
85649
- if ("grade" in this.selector) {
85650
- return `${this.selector.subject} grade ${this.selector.grade}`;
85651
- }
85652
- return `code "${this.selector.code}"`;
85653
- }
85654
- }
85655
- function resolveActivityCourse(courses, courseRef) {
85656
- let matches;
85657
- if ("grade" in courseRef) {
85658
- matches = courses.filter((c) => c.subject === courseRef.subject && c.grade === courseRef.grade);
85659
- } else {
85660
- matches = courses.filter((c) => c.courseCode === courseRef.code);
85661
- }
85662
- if (matches.length === 0) {
85663
- throw new ActivityCourseResolutionError("unknown_course", courseRef);
85664
- }
85665
- if (matches.length > 1) {
85666
- throw new ActivityCourseResolutionError("ambiguous_course", courseRef, matches.length);
85667
- }
85668
- return matches[0];
85669
- }
85670
- // src/server/lib/build-activity-events.ts
85671
- class MissingSyncedCourseIdError extends Error {
85672
- course;
85673
- env;
85674
- constructor(course, env) {
85675
- const identifier = course.grade === undefined ? course.courseCode ?? course.subject : `${course.subject} grade ${course.grade}`;
85676
- super(`Course "${identifier}" is missing a synced ID for ${env}. Run \`timeback sync\` first.`);
85677
- this.name = "MissingSyncedCourseIdError";
85678
- this.course = course;
85679
- this.env = env;
85680
- }
85681
- }
85682
- function buildCourseId(course, apiEnv) {
85683
- const courseId = course.ids?.[apiEnv];
85684
- if (!courseId) {
85685
- throw new MissingSyncedCourseIdError(course, apiEnv);
85686
- }
85687
- return courseId;
85688
- }
85689
- function buildCourseName(course) {
85690
- if (course.courseCode) {
85691
- return course.courseCode;
85692
- }
85693
- if (course.grade !== undefined) {
85694
- return `${course.subject} G${String(course.grade)}`;
85695
- }
85696
- return course.subject;
85697
- }
85698
-
85699
- class InvalidSensorUrlError extends Error {
85700
- sensor;
85701
- constructor(sensor) {
85702
- super(`Invalid sensor URL "${sensor}". Sensor must be a valid absolute URL (e.g., "https://sensor.example.com") to support slug-based activity IDs.`);
85703
- this.name = "InvalidSensorUrlError";
85704
- this.sensor = sensor;
85705
- }
85706
- }
85707
- function buildCanonicalActivityUrl(sensor, selector, slug) {
85708
- let base;
85709
- try {
85710
- base = new URL(sensor);
85711
- } catch {
85712
- throw new InvalidSensorUrlError(sensor);
85713
- }
85714
- const pathSegment = "grade" in selector ? `${selector.subject}/g${String(selector.grade)}` : selector.code;
85715
- const basePath = base.pathname.replace(/\/+$/, "");
85716
- base.pathname = `${basePath}/activities/${pathSegment}/${encodeURIComponent(slug)}`;
85717
- return base.toString();
85718
- }
85719
- function buildActivityContext(payload, course, appName, apiEnv, sensor) {
85720
- return {
85721
- id: buildCanonicalActivityUrl(sensor, payload.course, payload.id),
85722
- type: "TimebackActivityContext",
85723
- subject: course.subject,
85724
- app: { name: appName },
85725
- activity: { name: payload.name },
85726
- course: {
85727
- id: buildCourseId(course, apiEnv),
85728
- name: buildCourseName(course)
85729
- }
85730
- };
85731
- }
85732
- function buildActivityMetrics(metrics) {
85733
- const result = [];
85734
- if (metrics.totalQuestions !== undefined) {
85735
- result.push({ type: "totalQuestions", value: metrics.totalQuestions });
85736
- }
85737
- if (metrics.correctQuestions !== undefined) {
85738
- result.push({ type: "correctQuestions", value: metrics.correctQuestions });
85739
- }
85740
- if (metrics.xpEarned !== undefined) {
85741
- result.push({ type: "xpEarned", value: metrics.xpEarned });
85742
- }
85743
- if (metrics.masteredUnits !== undefined) {
85744
- result.push({ type: "masteredUnits", value: metrics.masteredUnits });
85745
- }
85746
- return result;
85747
- }
85748
- function buildTimeSpentMetrics(elapsedMs, pausedMs) {
85749
- const result = [{ type: "active", value: Math.max(0, elapsedMs) / 1000 }];
85750
- if (pausedMs > 0) {
85751
- result.push({ type: "inactive", value: Math.max(0, pausedMs) / 1000 });
85752
- }
85753
- return result;
85754
- }
85755
- async function sendCaliperEnvelope(client, sensor, activityEvent, timeSpentEvent) {
85756
- await client.caliper.events.send(sensor, [activityEvent, timeSpentEvent]);
85757
- }
85758
- // src/server/lib/build-user-profile.ts
85759
- function buildCourseLookup(courses, apiEnv) {
85760
- const courseById = new Map;
85761
- for (const course of courses) {
85762
- const courseId = course.ids?.[apiEnv];
85763
- if (courseId) {
85764
- courseById.set(courseId, course);
85765
- }
85766
- }
85767
- return courseById;
85768
- }
85769
- function mapEnrollmentsToCourses(enrollments, courseById) {
85770
- return enrollments.map((enrollment) => {
85771
- const configuredCourse = courseById.get(enrollment.course.id);
85772
- return {
85773
- id: enrollment.course.id,
85774
- code: configuredCourse?.courseCode ?? enrollment.course.id,
85775
- name: enrollment.course.title
85776
- };
85777
- });
85778
- }
85779
- function pickGoalsFromEnrollments(enrollments) {
85780
- return enrollments.map((enrollment) => enrollment.metadata?.goals).find(Boolean);
85781
- }
85782
- function getUtcDayRange(date6) {
85783
- const start = new Date(Date.UTC(date6.getUTCFullYear(), date6.getUTCMonth(), date6.getUTCDate()));
85784
- const end = new Date(Date.UTC(date6.getUTCFullYear(), date6.getUTCMonth(), date6.getUTCDate(), 23, 59, 59, 999));
85785
- return { start, end };
85786
- }
85787
- function sumXp(facts) {
85788
- return Object.values(facts).reduce((dateTotal, subjects) => {
85789
- return dateTotal + Object.values(subjects).reduce((subjectTotal, metrics) => {
85790
- return subjectTotal + (metrics.activityMetrics?.xpEarned ?? 0);
85791
- }, 0);
85792
- }, 0);
85793
- }
85794
85687
  // src/shared/constants.ts
85795
85688
  var ROUTES = {
85796
85689
  ACTIVITY: "/activity",
@@ -85800,10 +85693,83 @@ var ROUTES = {
85800
85693
  CALLBACK: "/identity/callback"
85801
85694
  },
85802
85695
  USER: {
85803
- ME: "/user/me"
85696
+ ME: "/user/me",
85697
+ VERIFY: "/user/verify"
85804
85698
  }
85805
85699
  };
85806
85700
 
85701
+ // src/server/lib/sso.ts
85702
+ function buildErrorContext(error57, errorCode, state, req) {
85703
+ return {
85704
+ error: error57,
85705
+ errorCode,
85706
+ state,
85707
+ req,
85708
+ redirect: redirectResponse,
85709
+ json: jsonResponse
85710
+ };
85711
+ }
85712
+ function tryDecodeState(stateParam) {
85713
+ try {
85714
+ return decodeBase64Url(stateParam);
85715
+ } catch {
85716
+ ssoLog.warn("Failed to decode state");
85717
+ return;
85718
+ }
85719
+ }
85720
+ function handleIdpError(errorParam, url6, state, req, onCallbackError) {
85721
+ const errorDesc = url6.searchParams.get("error_description");
85722
+ ssoLog.error("IdP returned error", { error: errorParam, description: errorDesc });
85723
+ const error57 = new Error(errorDesc ?? errorParam);
85724
+ if (onCallbackError) {
85725
+ return onCallbackError(buildErrorContext(error57, errorParam, state, req));
85726
+ }
85727
+ return jsonResponse({ error: errorParam }, 400);
85728
+ }
85729
+ function handleMissingCode(state, req, onCallbackError) {
85730
+ ssoLog.error("Missing authorization code in callback");
85731
+ const error57 = new Error("Missing authorization code");
85732
+ if (onCallbackError) {
85733
+ return onCallbackError(buildErrorContext(error57, "missing_code", state, req));
85734
+ }
85735
+ return jsonResponse({ error: "Missing authorization code" }, 400);
85736
+ }
85737
+ async function initiateSignIn(params) {
85738
+ const { req, env, clientId, buildState } = params;
85739
+ const issuer = params.issuer ?? getIssuer(env);
85740
+ const url6 = new URL(req.url);
85741
+ let redirectUri = params.redirectUri;
85742
+ if (!redirectUri) {
85743
+ const basePath = url6.pathname.replace(ROUTES.IDENTITY.SIGNIN, "");
85744
+ redirectUri = `${url6.origin}${basePath}${ROUTES.IDENTITY.CALLBACK}`;
85745
+ }
85746
+ ssoLog.debug("SSO sign-in initiated", { env, issuer, clientId, redirectUri });
85747
+ const stateData = buildState ? buildState({ req, url: url6 }) : {};
85748
+ const state = encodeBase64Url(stateData);
85749
+ const authUrl = await buildAuthorizationUrl({
85750
+ issuer,
85751
+ clientId,
85752
+ redirectUri,
85753
+ state
85754
+ });
85755
+ return redirectResponse(authUrl);
85756
+ }
85757
+ function parseCallback(req) {
85758
+ const url6 = new URL(req.url);
85759
+ const code = url6.searchParams.get("code");
85760
+ const errorParam = url6.searchParams.get("error");
85761
+ const stateParam = url6.searchParams.get("state");
85762
+ ssoLog.debug("Received callback from IdP", { hasCode: !!code, error: errorParam });
85763
+ const state = stateParam ? tryDecodeState(stateParam) : undefined;
85764
+ return { url: url6, code, errorParam, state };
85765
+ }
85766
+ function computeRedirectUri(url6, configuredRedirectUri) {
85767
+ if (configuredRedirectUri) {
85768
+ return configuredRedirectUri;
85769
+ }
85770
+ const basePath = url6.pathname.replace(ROUTES.IDENTITY.CALLBACK, "");
85771
+ return `${url6.origin}${basePath}${ROUTES.IDENTITY.CALLBACK}`;
85772
+ }
85807
85773
  // src/server/adapters/utils.ts
85808
85774
  function normalizePathname(path) {
85809
85775
  const raw = path.trim();
@@ -85853,6 +85819,8 @@ function matchTimebackRoute(params) {
85853
85819
  return "identity.signOut";
85854
85820
  if (relative === ROUTES.USER.ME)
85855
85821
  return "user.me";
85822
+ if (relative === ROUTES.USER.VERIFY)
85823
+ return "user.verify";
85856
85824
  }
85857
85825
  if (method === "POST") {
85858
85826
  if (relative === ROUTES.ACTIVITY)
@@ -85878,6 +85846,8 @@ function matchTimebackRoute(params) {
85878
85846
  return "identity.signOut";
85879
85847
  if (pathname.endsWith(ROUTES.USER.ME))
85880
85848
  return "user.me";
85849
+ if (pathname.endsWith(ROUTES.USER.VERIFY))
85850
+ return "user.verify";
85881
85851
  }
85882
85852
  if (method === "POST") {
85883
85853
  if (pathname.endsWith(ROUTES.ACTIVITY))
@@ -85918,6 +85888,11 @@ function toTanStackStartHandler(input) {
85918
85888
  return jsonResponse({ error: "Not found" }, 404);
85919
85889
  return handle.user.me(event.request);
85920
85890
  }
85891
+ if (route === "user.verify") {
85892
+ if (!hasUserHandler(handle))
85893
+ return jsonResponse({ error: "Not found" }, 404);
85894
+ return handle.user.verify(event.request);
85895
+ }
85921
85896
  if (route === "activity") {
85922
85897
  if (!hasActivityHandler(handle))
85923
85898
  return jsonResponse({ error: "Not found" }, 404);
@@ -12,7 +12,7 @@ type AnyHandlers = Handlers | IdentityOnlyHandlers;
12
12
  /**
13
13
  * A canonical route identifier for Timeback's built-in endpoints.
14
14
  */
15
- type TimebackRouteId = 'identity.signIn' | 'identity.callback' | 'identity.signOut' | 'user.me' | 'activity';
15
+ type TimebackRouteId = 'identity.signIn' | 'identity.callback' | 'identity.signOut' | 'user.me' | 'user.verify' | 'activity';
16
16
  /**
17
17
  * Normalize a path-like string to a pathname only (no query string).
18
18
  *
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/server/adapters/utils.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAC9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE/C;;GAEG;AACH,KAAK,WAAW,GAAG,QAAQ,GAAG,oBAAoB,CAAA;AAElD;;GAEG;AACH,KAAK,eAAe,GACjB,iBAAiB,GACjB,mBAAmB,GACnB,kBAAkB,GAClB,SAAS,GACT,UAAU,CAAA;AAEb;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAqBtD;AA8BD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IAC1C,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,MAAM,CAAA;CACrB,GAAG,eAAe,GAAG,IAAI,CAsDzB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,WAAW,CAEhE;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,WAAW,GAAG,QAAQ,IAAI,QAAQ,CAE9E;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,WAAW,GAAG,QAAQ,IAAI,QAAQ,CAE1E"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/server/adapters/utils.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAC9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE/C;;GAEG;AACH,KAAK,WAAW,GAAG,QAAQ,GAAG,oBAAoB,CAAA;AAElD;;GAEG;AACH,KAAK,eAAe,GACjB,iBAAiB,GACjB,mBAAmB,GACnB,kBAAkB,GAClB,SAAS,GACT,aAAa,GACb,UAAU,CAAA;AAEb;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAqBtD;AA8BD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IAC1C,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,MAAM,CAAA;CACrB,GAAG,eAAe,GAAG,IAAI,CAwDzB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,WAAW,CAEhE;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,WAAW,GAAG,QAAQ,IAAI,QAAQ,CAE9E;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,WAAW,GAAG,QAAQ,IAAI,QAAQ,CAE1E"}
@@ -1,7 +1,13 @@
1
1
  import type { ActivityCompletedEvent, TimebackActivityContext, TimebackActivityMetric, TimeSpentEvent, TimeSpentMetric } from '@timeback/caliper';
2
2
  import type { TimebackClient } from '@timeback/core';
3
- import type { ActivityEndPayload, ActivityMetrics } from '../../shared/types';
4
- import type { AppConfig } from '../types';
3
+ import type { ActivityCourseRef, ActivityMetrics } from '../../../shared/types';
4
+ import type { ActivityBeforeSendData, AppConfig } from '../../types';
5
+ import type { ValidatedActivityPayload } from './types';
6
+ /**
7
+ * Activity Caliper Events
8
+ *
9
+ * Build and send Caliper events for activity submissions.
10
+ */
5
11
  /**
6
12
  * Error thrown when a course is missing a synced ID for the target environment.
7
13
  */
@@ -39,7 +45,11 @@ export declare class InvalidSensorUrlError extends Error {
39
45
  * @param sensor - Sensor URL for building the canonical activity URL
40
46
  * @returns Caliper activity context payload
41
47
  */
42
- export declare function buildActivityContext(payload: ActivityEndPayload, course: AppConfig['courses'][number], appName: string, apiEnv: 'staging' | 'production', sensor: string): TimebackActivityContext;
48
+ export declare function buildActivityContext(payload: {
49
+ id: string;
50
+ name: string;
51
+ course: ActivityCourseRef;
52
+ }, course: AppConfig['courses'][number], appName: string, apiEnv: 'staging' | 'production', sensor: string): TimebackActivityContext;
43
53
  /**
44
54
  * Build Caliper activity metrics from the client payload.
45
55
  *
@@ -55,6 +65,21 @@ export declare function buildActivityMetrics(metrics: ActivityMetrics): Timeback
55
65
  * @returns Normalized Caliper time spent metrics
56
66
  */
57
67
  export declare function buildTimeSpentMetrics(elapsedMs: number, pausedMs: number): TimeSpentMetric[];
68
+ /**
69
+ * Build Caliper events for an activity.
70
+ *
71
+ * @param params - Event building parameters
72
+ * @returns Built events and context
73
+ */
74
+ export declare function buildActivityEvents(params: {
75
+ sensor: string;
76
+ timebackId: string;
77
+ email: string;
78
+ payload: ValidatedActivityPayload;
79
+ course: AppConfig['courses'][number];
80
+ appName: string;
81
+ apiEnv: 'staging' | 'production';
82
+ }): ActivityBeforeSendData;
58
83
  /**
59
84
  * Send a batch of Caliper events as a single envelope.
60
85
  *
@@ -64,4 +89,4 @@ export declare function buildTimeSpentMetrics(elapsedMs: number, pausedMs: numbe
64
89
  * @param timeSpentEvent - TimeSpentEvent payload
65
90
  */
66
91
  export declare function sendCaliperEnvelope(client: TimebackClient, sensor: string, activityEvent: ActivityCompletedEvent, timeSpentEvent: TimeSpentEvent): Promise<void>;
67
- //# sourceMappingURL=build-activity-events.d.ts.map
92
+ //# sourceMappingURL=caliper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"caliper.d.ts","sourceRoot":"","sources":["../../../../src/server/handlers/activity/caliper.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACX,sBAAsB,EACtB,uBAAuB,EACvB,sBAAsB,EAEtB,cAAc,EACd,eAAe,EACf,MAAM,mBAAmB,CAAA;AAC1B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAC/E,OAAO,KAAK,EAAE,sBAAsB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACpE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,SAAS,CAAA;AAEvD;;;;GAIG;AAMH;;GAEG;AACH,qBAAa,0BAA2B,SAAQ,KAAK;IACpD,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAA;IAC7C,QAAQ,CAAC,GAAG,EAAE,SAAS,GAAG,YAAY,CAAA;IAEtC,YAAY,MAAM,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,SAAS,GAAG,YAAY,EAW9E;CACD;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IAEvB,YAAY,MAAM,EAAE,MAAM,EAMzB;CACD;AAiHD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,oBAAoB,CACnC,OAAO,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE,EAChE,MAAM,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,EACpC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,SAAS,GAAG,YAAY,EAChC,MAAM,EAAE,MAAM,GACZ,uBAAuB,CAmBzB;AAMD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,eAAe,GAAG,sBAAsB,EAAE,CAoBvF;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,eAAe,EAAE,CAQ5F;AAMD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAC3C,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,wBAAwB,CAAA;IACjC,MAAM,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAA;IACpC,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,SAAS,GAAG,YAAY,CAAA;CAChC,GAAG,sBAAsB,CA4CzB;AAMD;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CACxC,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,sBAAsB,EACrC,cAAc,EAAE,cAAc,GAC5B,OAAO,CAAC,IAAI,CAAC,CAEf"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Activity Gradebook (OneRoster)
3
+ *
4
+ * Write assessment line items and results for activity submissions.
5
+ *
6
+ * Attempt semantics:
7
+ * - `attempt` is the primary identity (1-based) stored in `metadata.attempt`
8
+ * - Results are written via PUT upsert using a deterministic `sourcedId`
9
+ * - Retry detection: if `payload.endedAt` matches an existing result's `scoreDate` (and `metadata.endedAt`),
10
+ * the same attempt number is reused to make retries idempotent
11
+ * - New attempt: if no matching `endedAt` is found, attempt is `max(existingAttempt) + 1`
12
+ *
13
+ * Concurrency note:
14
+ * - Two concurrent "new attempts" for the same (lineItem, student) can race.
15
+ * - We avoid silent overwrites by including `endedAt` in the result `sourcedId` so two distinct submissions
16
+ * never share an identifier even if they compute the same attempt number.
17
+ */
18
+ import type { TimebackClient } from '@timeback/core';
19
+ import type { ValidatedActivityPayload } from './types';
20
+ /**
21
+ * Resolve the attempt number for a gradebook result.
22
+ *
23
+ * - If an existing result has `scoreDate === endedAt` or `metadata.endedAt === endedAt`,
24
+ * return its `metadata.attempt` (retry of the same attempt).
25
+ * - Otherwise, return `max(metadata.attempt) + 1` (new attempt).
26
+ *
27
+ * @param client - Timeback client
28
+ * @param lineItemId - The assessment line item sourcedId
29
+ * @param timebackId - The student's Timeback user sourcedId
30
+ * @param endedAt - The activity end timestamp from the client payload
31
+ * @returns The attempt number to use (1-based)
32
+ */
33
+ export declare function resolveAttemptNumber(client: TimebackClient, lineItemId: string, timebackId: string, endedAt: string): Promise<number>;
34
+ /**
35
+ * Write gradebook entry (assessment line item + result) for an activity.
36
+ *
37
+ * This is best-effort: if the write fails, we log and continue.
38
+ * Gradebook writes are only performed when both totalQuestions and correctQuestions are present.
39
+ *
40
+ * Result identity is based on attempt number:
41
+ * - `resultId = lineItemId:studentId:attempt-N`
42
+ * - Retries of the same attempt (same `endedAt`) reuse the same result ID
43
+ * - New attempts get an incremented attempt number
44
+ *
45
+ * @param params - Gradebook write parameters
46
+ */
47
+ export declare function writeGradebookEntry(params: {
48
+ client: TimebackClient;
49
+ courseId: string;
50
+ activityId: string;
51
+ activityName: string;
52
+ timebackId: string;
53
+ payload: ValidatedActivityPayload;
54
+ appName: string;
55
+ }): Promise<void>;
56
+ //# sourceMappingURL=gradebook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gradebook.d.ts","sourceRoot":"","sources":["../../../../src/server/handlers/activity/gradebook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAGpD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,SAAS,CAAA;AAkEvD;;;;;;;;;;;;GAYG;AACH,wBAAsB,oBAAoB,CACzC,MAAM,EAAE,cAAc,EACtB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAuCjB;AAyMD;;;;;;;;;;;;GAYG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IACjD,MAAM,EAAE,cAAc,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,wBAAwB,CAAA;IACjC,OAAO,EAAE,MAAM,CAAA;CACf,GAAG,OAAO,CAAC,IAAI,CAAC,CAiDhB"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Activity Handler
3
+ *
4
+ * HTTP route handler for activity submissions.
5
+ * Orchestrates validation, user resolution, gradebook writes, and Caliper event sending.
6
+ */
7
+ import type { ActivityHandler, ActivityHandlerConfig } from './types';
8
+ /**
9
+ * Create the activity POST handler.
10
+ *
11
+ * @param config - Handler configuration
12
+ * @returns The activity request handler
13
+ */
14
+ export declare function createActivityHandler(config: ActivityHandlerConfig): ActivityHandler;
15
+ //# sourceMappingURL=handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../../src/server/handlers/activity/handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAkBH,OAAO,KAAK,EAAE,eAAe,EAAE,qBAAqB,EAAoB,MAAM,SAAS,CAAA;AAmFvF;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,eAAe,CAuGpF"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Activity Handler
3
+ *
4
+ * Route handler for activity tracking and submission.
5
+ */
6
+ export { createActivityHandler } from './handler';
7
+ export { buildActivityContext, buildActivityMetrics, buildTimeSpentMetrics, InvalidSensorUrlError, MissingSyncedCourseIdError, sendCaliperEnvelope, } from './caliper';
8
+ export type { ActivityHandler, ActivityHandlerConfig, ActivityUserInfo, ValidatedActivityPayload, ValidationResult, } from './types';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/server/handlers/activity/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAA;AAEjD,OAAO,EACN,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,0BAA0B,EAC1B,mBAAmB,GACnB,MAAM,WAAW,CAAA;AAElB,YAAY,EACX,eAAe,EACf,qBAAqB,EACrB,gBAAgB,EAChB,wBAAwB,EACxB,gBAAgB,GAChB,MAAM,SAAS,CAAA"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Activity Course Resolution
3
+ *
4
+ * Resolves course selectors from activity payloads to configured courses.
5
+ */
6
+ import type { ActivityCourseRef } from '../../../shared/types';
7
+ import type { AppConfig } from '../../types';
8
+ /**
9
+ * Error thrown when a course selector cannot be resolved against config.
10
+ */
11
+ export declare class ActivityCourseResolutionError extends Error {
12
+ readonly code: 'unknown_course' | 'ambiguous_course';
13
+ readonly selector: ActivityCourseRef;
14
+ readonly count?: number;
15
+ constructor(code: ActivityCourseResolutionError['code'], selector: ActivityCourseRef, count?: number);
16
+ /**
17
+ * Get a human-readable description of the selector.
18
+ *
19
+ * @returns Human-readable selector description
20
+ */
21
+ get selectorDescription(): string;
22
+ }
23
+ /**
24
+ * Resolve a course config entry from an activity course selector.
25
+ *
26
+ * **Use case:** Activity handler — match the client's course selector
27
+ * to a configured course in `timeback.config.json`.
28
+ *
29
+ * Supports two selector modes:
30
+ * - **subjectGrade**: Match by `(subject, grade)`
31
+ * - **courseCode**: Match by `courseCode`
32
+ *
33
+ * @param courses - Configured courses from `timeback.config.json`
34
+ * @param courseRef - Course selector from the client payload
35
+ * @returns Matched course config entry
36
+ * @throws {ActivityCourseResolutionError} When selector is unknown or ambiguous
37
+ */
38
+ export declare function resolveActivityCourse(courses: AppConfig['courses'], courseRef: ActivityCourseRef): AppConfig['courses'][number];
39
+ //# sourceMappingURL=resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../../../src/server/handlers/activity/resolve.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AAC9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE5C;;GAEG;AACH,qBAAa,6BAA8B,SAAQ,KAAK;IACvD,QAAQ,CAAC,IAAI,EAAE,gBAAgB,GAAG,kBAAkB,CAAA;IACpD,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAA;IACpC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IAEvB,YACC,IAAI,EAAE,6BAA6B,CAAC,MAAM,CAAC,EAC3C,QAAQ,EAAE,iBAAiB,EAC3B,KAAK,CAAC,EAAE,MAAM,EAMd;IAED;;;;OAIG;IACH,IAAI,mBAAmB,IAAI,MAAM,CAKhC;CACD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,CACpC,OAAO,EAAE,SAAS,CAAC,SAAS,CAAC,EAC7B,SAAS,EAAE,iBAAiB,GAC1B,SAAS,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAoB9B"}