@playcademy/sandbox 0.12.2-beta.3 → 0.12.2-beta.5

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 (3) hide show
  1. package/dist/cli.js +428 -99
  2. package/dist/server.js +428 -99
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2517,7 +2517,7 @@ var package_default;
2517
2517
  var init_package = __esm(() => {
2518
2518
  package_default = {
2519
2519
  name: "@playcademy/sandbox",
2520
- version: "0.12.2-beta.3",
2520
+ version: "0.12.2-beta.5",
2521
2521
  description: "Local development server for Playcademy game development",
2522
2522
  type: "module",
2523
2523
  exports: {
@@ -20917,6 +20917,7 @@ var init_table8 = __esm(() => {
20917
20917
  gameId: uuid("game_id").notNull().references(() => games.id, { onDelete: "cascade" }),
20918
20918
  parentOrigin: text("parent_origin").notNull(),
20919
20919
  timebackCourseId: text("timeback_course_id"),
20920
+ contextId: text("context_id"),
20920
20921
  passHash: text("pass_hash").unique(),
20921
20922
  passExpiresAt: timestamp("pass_expires_at", { withTimezone: true }).notNull(),
20922
20923
  redeemedAt: timestamp("redeemed_at", { withTimezone: true }),
@@ -50823,13 +50824,16 @@ function isCanonicalOrigin(value) {
50823
50824
  }
50824
50825
  return url.protocol === "https:" || url.protocol === "http:" && isLocalHostname(url.hostname);
50825
50826
  }
50826
- var LOCAL_HOSTNAMES, EmbedOriginSchema, EmbedPartnerClientIdSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
50827
+ var LOCAL_HOSTNAMES, EmbedOriginSchema, EmbedContextIdSchema, EmbedPartnerClientIdSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
50827
50828
  var init_schemas5 = __esm(() => {
50828
50829
  init_esm();
50829
50830
  LOCAL_HOSTNAMES = new Set(["localhost", "127.0.0.1"]);
50830
50831
  EmbedOriginSchema = stringType().refine(isCanonicalOrigin, {
50831
50832
  message: "Origin must be a bare https origin like https://example.com, with no path or slash"
50832
50833
  });
50834
+ EmbedContextIdSchema = stringType().regex(/^[A-Za-z0-9._~-]{1,128}$/, {
50835
+ message: "contextId must be 1-128 URL-safe characters (letters, digits, . _ ~ -)"
50836
+ });
50833
50837
  EmbedPartnerClientIdSchema = stringType().trim().min(1).max(200);
50834
50838
  EnsureEmbedPartnerSchema = objectType({
50835
50839
  name: stringType().trim().min(1).max(100)
@@ -50841,7 +50845,8 @@ var init_schemas5 = __esm(() => {
50841
50845
  timebackId: stringType().trim().min(1).max(200),
50842
50846
  gameSlug: stringType().trim().min(1).max(100),
50843
50847
  parentOrigin: EmbedOriginSchema,
50844
- timebackCourseId: stringType().trim().min(1).max(200).optional()
50848
+ timebackCourseId: stringType().trim().min(1).max(200).optional(),
50849
+ contextId: EmbedContextIdSchema.optional()
50845
50850
  }).strict();
50846
50851
  RedeemEmbedPassSchema = objectType({
50847
50852
  pass: stringType().min(1).max(200),
@@ -81865,6 +81870,9 @@ function normalizedIdentityCase2(value) {
81865
81870
  function frameworkAliasKey2(value) {
81866
81871
  return value.normalize("NFKC").toLocaleLowerCase("en-US").replace(/[^a-z0-9]+/g, "");
81867
81872
  }
81873
+ function isUuidShaped2(value) {
81874
+ return UUID_PATTERN2.test(value.trim());
81875
+ }
81868
81876
  function isCommonCoreMathIdentifier2(identifier) {
81869
81877
  const canonical = normalizedIdentityCase2(normalizedWhitespace2(identifier));
81870
81878
  return canonical.startsWith("CCSS.MATH.") || /^(?:K|[1-8])\.(?:CC|OA|NBT|NF|MD|RP|NS|EE|F|G|SP)\./.test(canonical) || /^(?:HS)?(?:N|A|F|G|S)-[A-Z]+\./.test(canonical) || /^MP\.?\d/.test(canonical);
@@ -81896,6 +81904,14 @@ function canonicalIdentifier2(framework, authoredIdentifier) {
81896
81904
  }
81897
81905
  return identifier;
81898
81906
  }
81907
+ function readableAlignmentIdentifier2(alignment) {
81908
+ const identifier = alignment.identifier?.trim() ?? "";
81909
+ if (identifier && !isUuidShaped2(identifier)) {
81910
+ return identifier;
81911
+ }
81912
+ const id = alignment.id.trim();
81913
+ return id && !isUuidShaped2(id) ? id : "";
81914
+ }
81899
81915
  function canonicalAssessmentStandardRef2(standard) {
81900
81916
  const authoredFramework = normalizedWhitespace2(standard.framework);
81901
81917
  const framework = canonicalFramework2(authoredFramework, standard.identifier);
@@ -82002,7 +82018,23 @@ function qtiStandardsFromMetadata(metadata2) {
82002
82018
  standards.push(...qtiAlignmentStandardsFromMetadata2(metadata2));
82003
82019
  return dedupeStandards2(standards);
82004
82020
  }
82005
- var EVENT_HANDLER_ATTRIBUTE, SCRIPT_SCHEMES2, DATA_RASTER_IMAGE_PATTERN2, QTI_MATHML_ALLOWED_TAGS2, metadataSymbol2, parser2, SUPPORTED_QTI_INTERACTION_TYPES2, URL_ATTRIBUTES3, IMAGE_URL_ATTRIBUTES3, INTERACTION_TYPES2, XML_NAMED_ENTITIES, EMPHASIS_TAGS2, STRONG_TAGS2, CONTENT_SKIPPED_TAGS2, BLOCK_CONTENT_KINDS2, GAP_MATCH_TOKEN_TAGS2, STAGE_GRAPHIC_EXCLUDED_CONTAINERS2, BLANK_SENTINEL2 = "￿", MARKED_BLANK_PATTERN2, MARKED_BLANK_GLOBAL_PATTERN2, REGION_SHAPE_COORDS, QTI_NAMESPACE = "http://www.imsglobal.org/xsd/imsqtiasi_v3p0", QTI_AUTHORING_BLANK_MARKER = "___", MATCH_CORRECT_TEMPLATE2 = "http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct", MAP_RESPONSE_TEMPLATE2 = "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response", MAP_RESPONSE_TEMPLATES2, RESPONSE_PROCESSING_TEMPLATES2, GLOBAL_BODY_ATTRIBUTES, BODY_ELEMENT_ATTRIBUTES, MATHML_ATTRIBUTES, VOID_BODY_ELEMENTS2, URL_ATTRIBUTES22, IMAGE_URL_ATTRIBUTES22, INTERACTION_ATTRIBUTES, CHOICE_ATTRIBUTES, CHOICE_ELEMENT_ATTRIBUTES, VOID_CHOICE_ELEMENTS2, NUMERIC_COMPARISON_ATTRIBUTES2, RESPONSE_CARDINALITIES2, RESPONSE_BASE_TYPES2, BLANK_SENTINEL22 = "￿", QTI_FLOAT_VALUE, PLAYABLE_POINT_VALUE, POINT_INTERACTION_TYPES2, INDEPENDENT_PROCESSING_TAGS2, PRIVATE_PLAYABLE_KEYS2, trimmedNonEmptyString2, finiteNumber22, positiveFiniteNumber2, IDENTIFIER_VALUE2, DIRECTED_PAIR_VALUE2, scoringMatchComparisonSchema2, scoringNumericEqualComparisonSchema2, scoringNumericEqualRoundedComparisonSchema2, scoringRuleSchema2, scoringRulesSchema2, scoringItemSchema2, assessmentScoringArtifactItemsSchema2, assessmentScoringArtifactSchema2, SUPPORTED_BASE_TYPES2, SUPPORTED_CARDINALITIES2, MATCH_CORRECT_TEMPLATES2, QtiScoringArtifactValidationError, COMMON_CORE_MATH_FRAMEWORK_ALIASES2, COMMON_CORE_ELA_FRAMEWORK_ALIASES2, COMMON_CORE_FRAMEWORK_ALIASES2;
82021
+ function qtiAssessmentStandardRefsFromMetadata2(metadata2) {
82022
+ const refs = new Map;
82023
+ for (const alignment of qtiAlignmentStandardsFromMetadata2(metadata2)) {
82024
+ const identifier = readableAlignmentIdentifier2(alignment);
82025
+ if (identifier) {
82026
+ const canonical = canonicalAssessmentStandardRef2({
82027
+ framework: alignment.source,
82028
+ identifier
82029
+ });
82030
+ if (canonical.framework && canonical.identifier) {
82031
+ refs.set(assessmentStandardRefKey2(canonical), canonical);
82032
+ }
82033
+ }
82034
+ }
82035
+ return [...refs.values()];
82036
+ }
82037
+ var EVENT_HANDLER_ATTRIBUTE, SCRIPT_SCHEMES2, DATA_RASTER_IMAGE_PATTERN2, QTI_MATHML_ALLOWED_TAGS2, metadataSymbol2, parser2, SUPPORTED_QTI_INTERACTION_TYPES2, URL_ATTRIBUTES3, IMAGE_URL_ATTRIBUTES3, INTERACTION_TYPES2, XML_NAMED_ENTITIES, EMPHASIS_TAGS2, STRONG_TAGS2, CONTENT_SKIPPED_TAGS2, BLOCK_CONTENT_KINDS2, GAP_MATCH_TOKEN_TAGS2, STAGE_GRAPHIC_EXCLUDED_CONTAINERS2, BLANK_SENTINEL2 = "￿", MARKED_BLANK_PATTERN2, MARKED_BLANK_GLOBAL_PATTERN2, REGION_SHAPE_COORDS, QTI_NAMESPACE = "http://www.imsglobal.org/xsd/imsqtiasi_v3p0", QTI_AUTHORING_BLANK_MARKER = "___", MATCH_CORRECT_TEMPLATE2 = "http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct", MAP_RESPONSE_TEMPLATE2 = "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response", MAP_RESPONSE_TEMPLATES2, RESPONSE_PROCESSING_TEMPLATES2, GLOBAL_BODY_ATTRIBUTES, BODY_ELEMENT_ATTRIBUTES, MATHML_ATTRIBUTES, VOID_BODY_ELEMENTS2, URL_ATTRIBUTES22, IMAGE_URL_ATTRIBUTES22, INTERACTION_ATTRIBUTES, CHOICE_ATTRIBUTES, CHOICE_ELEMENT_ATTRIBUTES, VOID_CHOICE_ELEMENTS2, NUMERIC_COMPARISON_ATTRIBUTES2, RESPONSE_CARDINALITIES2, RESPONSE_BASE_TYPES2, BLANK_SENTINEL22 = "￿", QTI_FLOAT_VALUE, PLAYABLE_POINT_VALUE, POINT_INTERACTION_TYPES2, INDEPENDENT_PROCESSING_TAGS2, PRIVATE_PLAYABLE_KEYS2, trimmedNonEmptyString2, finiteNumber22, positiveFiniteNumber2, IDENTIFIER_VALUE2, DIRECTED_PAIR_VALUE2, scoringMatchComparisonSchema2, scoringNumericEqualComparisonSchema2, scoringNumericEqualRoundedComparisonSchema2, scoringRuleSchema2, scoringRulesSchema2, scoringItemSchema2, assessmentScoringArtifactItemsSchema2, assessmentScoringArtifactSchema2, SUPPORTED_BASE_TYPES2, SUPPORTED_CARDINALITIES2, MATCH_CORRECT_TEMPLATES2, QtiScoringArtifactValidationError, UUID_PATTERN2, COMMON_CORE_MATH_FRAMEWORK_ALIASES2, COMMON_CORE_ELA_FRAMEWORK_ALIASES2, COMMON_CORE_FRAMEWORK_ALIASES2;
82006
82038
  var init_qti = __esm(() => {
82007
82039
  init_timeback4();
82008
82040
  init_timeback4();
@@ -82364,6 +82396,7 @@ var init_qti = __esm(() => {
82364
82396
  this.name = "QtiScoringArtifactValidationError";
82365
82397
  }
82366
82398
  };
82399
+ UUID_PATTERN2 = /^(?:urn:uuid:)?\{?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}?$/i;
82367
82400
  COMMON_CORE_MATH_FRAMEWORK_ALIASES2 = new Set([
82368
82401
  "ccssm",
82369
82402
  "ccssmath",
@@ -82573,7 +82606,7 @@ function pruneUnreadableStandards(standards) {
82573
82606
  return standards.filter(hasReadableStandardLabel);
82574
82607
  }
82575
82608
  function parseQuestionIndex(title) {
82576
- const match = title.match(/\bquestion[\s_-]*(\d+)\b/i);
82609
+ const match = title?.match(/\bquestion[\s_-]*(\d+)\b/i);
82577
82610
  const index = match ? Number(match[1]) : Number.NaN;
82578
82611
  return Number.isFinite(index) && index > 0 ? index : null;
82579
82612
  }
@@ -82607,14 +82640,15 @@ function qtiQuestionsByIndex(qtiQuestions) {
82607
82640
  }
82608
82641
  return questionsByIndex;
82609
82642
  }
82643
+ function normalizedQuestionScore(score, scorePercentile) {
82644
+ const rawValue = numberField(score) ?? numberField(scorePercentile);
82645
+ return rawValue !== null && rawValue > 1 ? rawValue / 100 : rawValue;
82646
+ }
82610
82647
  function getQuestionCorrectness(result) {
82611
82648
  if (!result) {
82612
82649
  return "unknown";
82613
82650
  }
82614
- const score = numberField(result.score);
82615
- const scorePercentile = numberField(result.scorePercentile);
82616
- const rawValue = score ?? scorePercentile;
82617
- const value = rawValue !== null && rawValue > 1 ? rawValue / 100 : rawValue;
82651
+ const value = normalizedQuestionScore(result.score, result.scorePercentile);
82618
82652
  if (value === null) {
82619
82653
  return "unknown";
82620
82654
  }
@@ -82878,10 +82912,71 @@ var init_timeback_mastery_completion_util = __esm(async () => {
82878
82912
  await init_errors8();
82879
82913
  });
82880
82914
 
82915
+ // ../api-core/src/utils/bounded-map.util.ts
82916
+ function setBounded(map, key, value, maxSize) {
82917
+ map.delete(key);
82918
+ if (map.size >= maxSize) {
82919
+ const oldestKey = map.keys().next().value;
82920
+ if (oldestKey !== undefined) {
82921
+ map.delete(oldestKey);
82922
+ }
82923
+ }
82924
+ map.set(key, value);
82925
+ }
82926
+
82927
+ // ../api-core/src/utils/timeback-qti-test-lookup.util.ts
82928
+ async function resolveQtiTestId(client, testName, parentLineItem) {
82929
+ const cacheKey = `${parentLineItem.sourcedId ?? ""}:${testName}`;
82930
+ const cached = cache.get(cacheKey);
82931
+ if (cached) {
82932
+ setBounded(cache, cacheKey, cached, MAX_CACHE_SIZE);
82933
+ return cached;
82934
+ }
82935
+ let resource = null;
82936
+ const resourceId = sourceIdFromRef(parentLineItem.componentResource);
82937
+ if (resourceId) {
82938
+ resource = await client.api.oneroster.resources.get(resourceId).catch(() => null);
82939
+ }
82940
+ for (const candidate of qtiIdCandidates(parentLineItem, resource)) {
82941
+ const test = await client.qtiApi.assessmentTests.get(candidate).catch(() => null);
82942
+ if (test?.identifier) {
82943
+ setBounded(cache, cacheKey, test.identifier, MAX_CACHE_SIZE);
82944
+ return test.identifier;
82945
+ }
82946
+ }
82947
+ const normalizedTestName = testName.trim();
82948
+ if (!normalizedTestName) {
82949
+ return "";
82950
+ }
82951
+ for (let page = 1;page <= TITLE_LOOKUP_MAX_PAGES; page++) {
82952
+ const list = await client.qtiApi.assessmentTests.list({
82953
+ query: normalizedTestName,
82954
+ limit: TITLE_LOOKUP_LIMIT,
82955
+ page,
82956
+ sort: "title",
82957
+ order: "asc"
82958
+ }).catch(() => null);
82959
+ const qtiTestId = findQtiAssessmentTestIdByTitle(list?.items ?? [], normalizedTestName);
82960
+ if (qtiTestId) {
82961
+ setBounded(cache, cacheKey, qtiTestId, MAX_CACHE_SIZE);
82962
+ return qtiTestId;
82963
+ }
82964
+ const itemCount = list?.items?.length ?? 0;
82965
+ if (!list || itemCount < TITLE_LOOKUP_LIMIT || typeof list.pages === "number" && page >= list.pages) {
82966
+ break;
82967
+ }
82968
+ }
82969
+ return "";
82970
+ }
82971
+ var MAX_CACHE_SIZE = 500, TITLE_LOOKUP_LIMIT = 50, TITLE_LOOKUP_MAX_PAGES = 5, cache;
82972
+ var init_timeback_qti_test_lookup_util = __esm(() => {
82973
+ init_timeback_grade_level_results_util();
82974
+ cache = new Map;
82975
+ });
82976
+
82881
82977
  // ../api-core/src/services/timeback-admin.service.ts
82882
82978
  class TimebackAdminService {
82883
82979
  deps;
82884
- qtiTestIdCache = new Map;
82885
82980
  static XP_PRECISION_FACTOR = 10;
82886
82981
  static RECENT_ACTIVITY_LIMIT = 20;
82887
82982
  static MAX_STUDENT_ACTIVITY_LIMIT = 200;
@@ -82896,9 +82991,6 @@ class TimebackAdminService {
82896
82991
  static GAME_METRICS_FETCH_TIMEOUT_MS = 1e4;
82897
82992
  static GAME_METRICS_LIST_FETCH_TIMEOUT_MS = 3000;
82898
82993
  static GAME_METRICS_RUN_IDS_PER_REQUEST = 50;
82899
- static MAX_QTI_TEST_ID_CACHE_SIZE = 500;
82900
- static QTI_TEST_TITLE_LOOKUP_LIMIT = 50;
82901
- static QTI_TEST_TITLE_LOOKUP_MAX_PAGES = 5;
82902
82994
  constructor(deps) {
82903
82995
  this.deps = deps;
82904
82996
  }
@@ -83758,68 +83850,6 @@ class TimebackAdminService {
83758
83850
  return { activities: activitiesWithGameMetrics, hasMore };
83759
83851
  });
83760
83852
  }
83761
- rememberQtiTestId(cacheKey, qtiTestId) {
83762
- if (!qtiTestId) {
83763
- this.qtiTestIdCache.delete(cacheKey);
83764
- return;
83765
- }
83766
- if (this.qtiTestIdCache.has(cacheKey)) {
83767
- this.qtiTestIdCache.delete(cacheKey);
83768
- } else if (this.qtiTestIdCache.size >= TimebackAdminService.MAX_QTI_TEST_ID_CACHE_SIZE) {
83769
- const oldestKey = this.qtiTestIdCache.keys().next().value;
83770
- if (oldestKey !== undefined) {
83771
- this.qtiTestIdCache.delete(oldestKey);
83772
- }
83773
- }
83774
- this.qtiTestIdCache.set(cacheKey, qtiTestId);
83775
- }
83776
- async resolveQtiTestIdForGradeLevelResult(client, testName, parentLineItem) {
83777
- const cacheKey = `${parentLineItem.sourcedId ?? ""}:${testName}`;
83778
- const cachedQtiTestId = cacheKey ? this.qtiTestIdCache.get(cacheKey) : undefined;
83779
- if (cachedQtiTestId) {
83780
- this.rememberQtiTestId(cacheKey, cachedQtiTestId);
83781
- return cachedQtiTestId;
83782
- }
83783
- let resource = null;
83784
- const resourceId = sourceIdFromRef(parentLineItem.componentResource);
83785
- if (resourceId) {
83786
- resource = await client.api.oneroster.resources.get(resourceId).catch(() => null);
83787
- }
83788
- for (const candidate of qtiIdCandidates(parentLineItem, resource)) {
83789
- const test = await client.qtiApi.assessmentTests.get(candidate).catch(() => null);
83790
- if (test?.identifier) {
83791
- if (cacheKey) {
83792
- this.rememberQtiTestId(cacheKey, test.identifier);
83793
- }
83794
- return test.identifier;
83795
- }
83796
- }
83797
- const normalizedTestName = testName.trim();
83798
- if (!normalizedTestName) {
83799
- return "";
83800
- }
83801
- for (let page = 1;page <= TimebackAdminService.QTI_TEST_TITLE_LOOKUP_MAX_PAGES; page++) {
83802
- const list = await client.qtiApi.assessmentTests.list({
83803
- query: normalizedTestName,
83804
- limit: TimebackAdminService.QTI_TEST_TITLE_LOOKUP_LIMIT,
83805
- page,
83806
- sort: "title",
83807
- order: "asc"
83808
- }).catch(() => null);
83809
- const qtiTestId = findQtiAssessmentTestIdByTitle(list?.items ?? [], normalizedTestName);
83810
- if (qtiTestId) {
83811
- if (cacheKey) {
83812
- this.rememberQtiTestId(cacheKey, qtiTestId);
83813
- }
83814
- return qtiTestId;
83815
- }
83816
- const itemCount = list?.items?.length ?? 0;
83817
- if (!list || itemCount < TimebackAdminService.QTI_TEST_TITLE_LOOKUP_LIMIT || typeof list.pages === "number" && page >= list.pages) {
83818
- break;
83819
- }
83820
- }
83821
- return "";
83822
- }
83823
83853
  async getGradeLevelTestResults(user, options) {
83824
83854
  return this.withClientTelemetry(async () => {
83825
83855
  const { gameId, courseId, studentId } = options;
@@ -83922,7 +83952,7 @@ class TimebackAdminService {
83922
83952
  }).then((page) => page.data) : [];
83923
83953
  const latestResultsByLineItem = latestAssessmentResultsByLineItem(questionResults);
83924
83954
  const testName = resolveGradeLevelTestName(parentResult, parentLineItem);
83925
- const qtiTestId = await this.resolveQtiTestIdForGradeLevelResult(client, testName, parentLineItem);
83955
+ const qtiTestId = await resolveQtiTestId(client, testName, parentLineItem);
83926
83956
  const qtiQuestions = qtiTestId ? await client.qtiApi.assessmentTests.getQuestions(qtiTestId).then((response) => response.questions ?? []).catch(() => []) : [];
83927
83957
  const questions = mapGradeLevelReviewQuestions({
83928
83958
  questionLineItems,
@@ -84616,6 +84646,7 @@ var init_timeback_admin_service = __esm(async () => {
84616
84646
  init_timeback_discrepancy_queue_util();
84617
84647
  init_timeback_game_metrics_comparison_util();
84618
84648
  init_timeback_grade_level_results_util();
84649
+ init_timeback_qti_test_lookup_util();
84619
84650
  init_timeback_util();
84620
84651
  await __promiseAll([
84621
84652
  init_dist5(),
@@ -92622,6 +92653,128 @@ var init_timeback_create_integration_util = __esm(() => {
92622
92653
  init_timeback_util();
92623
92654
  });
92624
92655
 
92656
+ // ../api-core/src/utils/timeback-hole-filling-context.util.ts
92657
+ function buildAssignmentResultWhere(options) {
92658
+ return {
92659
+ status: "active",
92660
+ "student.sourcedId": options.studentId,
92661
+ "metadata.resultType": options.resultType,
92662
+ "metadata.assignmentId": options.assignmentId
92663
+ };
92664
+ }
92665
+ function assignmentIdOf(record) {
92666
+ const value = metadataOf(record).assignmentId;
92667
+ if (typeof value === "number" && Number.isFinite(value)) {
92668
+ return String(value);
92669
+ }
92670
+ const text = stringField3(value).trim();
92671
+ return text || undefined;
92672
+ }
92673
+ function toTriggeringResult(result) {
92674
+ const metadata2 = metadataOf(result);
92675
+ const testType = normalizeTestType(metadata2.testType);
92676
+ const assignmentId = assignmentIdOf(result);
92677
+ const lineItemId = sourceIdFromRef(result.assessmentLineItem);
92678
+ if (!assignmentId || !lineItemId || result.scoreStatus !== SCORE_STATUS2.fullyGraded || !isAllowedGradeLevelTestType(testType)) {
92679
+ return null;
92680
+ }
92681
+ return {
92682
+ resultId: result.sourcedId ?? "",
92683
+ assignmentId,
92684
+ lineItemId,
92685
+ testName: stringField3(metadata2.testName) || lineItemId,
92686
+ testType,
92687
+ score: numberField(result.score),
92688
+ scoreDate: result.scoreDate ?? null,
92689
+ totalQuestions: numberField(metadata2.totalQuestions),
92690
+ correctQuestions: numberField(metadata2.correctQuestions)
92691
+ };
92692
+ }
92693
+ function isMissed(resultText, score, scorePercentile) {
92694
+ const marking = stringField3(resultText).trim().toLowerCase();
92695
+ if (marking) {
92696
+ return marking !== "correct";
92697
+ }
92698
+ const value = normalizedQuestionScore(score, scorePercentile);
92699
+ return value !== null && value < 1;
92700
+ }
92701
+ function missedQuestionsFromInlineResponses(result) {
92702
+ const responses = metadataOf(result).responses;
92703
+ const questions = isRecord6(responses) && Array.isArray(responses.questions) ? responses.questions : undefined;
92704
+ if (!questions?.length) {
92705
+ return;
92706
+ }
92707
+ return questions.flatMap((question) => {
92708
+ if (!isRecord6(question) || !isMissed(question.result, question.score)) {
92709
+ return [];
92710
+ }
92711
+ const questionNumber = numberField(question.questionNumber);
92712
+ const qtiItemId = stringField3(question.questionQtiIdentifier).trim();
92713
+ return questionNumber === null ? [] : [{ questionNumber, ...qtiItemId && { qtiItemId } }];
92714
+ });
92715
+ }
92716
+ function questionNumberFromLineItemId(lineItemId) {
92717
+ const match = lineItemId.match(/_q(\d+)$/);
92718
+ const value = match ? Number(match[1]) : Number.NaN;
92719
+ return Number.isInteger(value) && value > 0 ? value : null;
92720
+ }
92721
+ function missedQuestionsFromQuestionResults(questionResults) {
92722
+ return [...latestAssessmentResultsByLineItem(questionResults)].flatMap(([lineItemId, result]) => {
92723
+ const questionNumber = questionNumberFromLineItemId(lineItemId);
92724
+ const responses = metadataOf(result).responses;
92725
+ const response = isRecord6(responses) && isRecord6(responses.response) ? responses.response : undefined;
92726
+ if (questionNumber === null || !isMissed(response?.result, result.score, result.scorePercentile)) {
92727
+ return [];
92728
+ }
92729
+ const qtiItemId = stringField3(response?.questionQtiIdentifier).trim();
92730
+ return [{ questionNumber, ...qtiItemId && { qtiItemId } }];
92731
+ }).toSorted((a, b) => a.questionNumber - b.questionNumber);
92732
+ }
92733
+ function qtiItemIdForQuestionNumber(qtiQuestions) {
92734
+ const byNumber = qtiQuestionsByIndex(qtiQuestions);
92735
+ return (questionNumber) => {
92736
+ const question = byNumber.get(questionNumber) ?? qtiQuestions[questionNumber - 1];
92737
+ return question ? question.question.identifier || question.reference.identifier : undefined;
92738
+ };
92739
+ }
92740
+ function groupMissedStandards(missed, standardsByItemId, itemIdFor) {
92741
+ const byKey = new Map;
92742
+ const untagged = [];
92743
+ for (const question of missed) {
92744
+ const itemId = question.qtiItemId ?? itemIdFor(question.questionNumber);
92745
+ const standards = itemId ? standardsByItemId.get(itemId) ?? [] : [];
92746
+ if (!standards.length) {
92747
+ untagged.push(question.questionNumber);
92748
+ }
92749
+ for (const standard of standards) {
92750
+ const key = `${standard.framework}\x00${standard.identifier}`;
92751
+ const entry = byKey.get(key) ?? {
92752
+ framework: standard.framework,
92753
+ code: standard.identifier,
92754
+ questionNumbers: [],
92755
+ qtiItemIds: []
92756
+ };
92757
+ entry.questionNumbers.push(question.questionNumber);
92758
+ if (itemId && !entry.qtiItemIds.includes(itemId)) {
92759
+ entry.qtiItemIds.push(itemId);
92760
+ }
92761
+ byKey.set(key, entry);
92762
+ }
92763
+ }
92764
+ return {
92765
+ missedStandards: [...byKey.values()].toSorted((a, b) => b.questionNumbers.length - a.questionNumbers.length || a.code.localeCompare(b.code, undefined, { numeric: true })),
92766
+ untaggedQuestionNumbers: untagged.toSorted((a, b) => a - b)
92767
+ };
92768
+ }
92769
+ function standardsFromQtiItem(item) {
92770
+ return qtiAssessmentStandardRefsFromMetadata2(metadataOf(item));
92771
+ }
92772
+ var init_timeback_hole_filling_context_util = __esm(() => {
92773
+ init_constants2();
92774
+ init_qti();
92775
+ init_timeback_grade_level_results_util();
92776
+ });
92777
+
92625
92778
  // ../api-core/src/utils/timeback-promotion.util.ts
92626
92779
  async function promoteCompletedCourse({
92627
92780
  db,
@@ -92751,14 +92904,20 @@ var init_timeback_service = __esm(async () => {
92751
92904
  init_helpers_index();
92752
92905
  init_tables_index();
92753
92906
  init_spans();
92907
+ init_constants2();
92754
92908
  init_types2();
92755
92909
  init_errors2();
92756
92910
  init_timeback_create_integration_util();
92911
+ init_timeback_grade_level_results_util();
92912
+ init_timeback_hole_filling_context_util();
92757
92913
  init_timeback_promotion_util();
92914
+ init_timeback_qti_test_lookup_util();
92758
92915
  init_timeback_removed_integration_util();
92759
92916
  init_timeback_util();
92760
92917
  await init_dist5();
92761
92918
  TimebackService = class TimebackService {
92919
+ static HOLE_FILLING_CONTEXT_TTL_MS = 10 * 60 * 1000;
92920
+ static HOLE_FILLING_CONTEXT_CACHE_SIZE = 1000;
92762
92921
  static HEARTBEAT_DEDUPE_TTL_MS = 5 * 60 * 1000;
92763
92922
  static processedHeartbeatWindows = new Map;
92764
92923
  static inFlightHeartbeatWindows = new Map;
@@ -92766,6 +92925,7 @@ var init_timeback_service = __esm(async () => {
92766
92925
  static COMPLETION_IN_FLIGHT_SETTLE_DELAY_MS = 400;
92767
92926
  static COMPLETION_IN_FLIGHT_SETTLE_ATTEMPTS = 5;
92768
92927
  deps;
92928
+ holeFillingContextCache = new Map;
92769
92929
  static cleanHeartbeatDedupeCache(now = Date.now()) {
92770
92930
  for (const [key, timestamp] of this.processedHeartbeatWindows) {
92771
92931
  if (now - timestamp > this.HEARTBEAT_DEDUPE_TTL_MS) {
@@ -93139,19 +93299,163 @@ var init_timeback_service = __esm(async () => {
93139
93299
  }
93140
93300
  async getUserEnrollments(userId, gameId) {
93141
93301
  return this.withClientTelemetry(async () => {
93142
- const userData = await this.deps.db.query.users.findFirst({
93143
- where: eq(users.id, userId),
93144
- columns: { id: true, timebackId: true }
93145
- });
93146
- if (!userData) {
93147
- throw new NotFoundError("User", userId);
93302
+ const enrollments = await this.fetchEnrollments(await this.requireTimebackId(userId));
93303
+ return gameId ? filterEnrollmentsByGame(enrollments, gameId) : enrollments;
93304
+ });
93305
+ }
93306
+ async requireTimebackId(userId) {
93307
+ const userData = await this.deps.db.query.users.findFirst({
93308
+ where: eq(users.id, userId),
93309
+ columns: { id: true, timebackId: true }
93310
+ });
93311
+ if (!userData) {
93312
+ throw new NotFoundError("User", userId);
93313
+ }
93314
+ if (!userData.timebackId) {
93315
+ throw new NotFoundError("Timeback account not found for user");
93316
+ }
93317
+ return userData.timebackId;
93318
+ }
93319
+ async getHoleFillingContext(userId, gameId) {
93320
+ return this.withClientTelemetry(async () => {
93321
+ setAttribute("app.timeback.operation", "get_hole_filling_context");
93322
+ const studentId = await this.requireTimebackId(userId);
93323
+ const enrollments = filterEnrollmentsByGame(await this.fetchEnrollments(studentId), gameId);
93324
+ const holeFilling = enrollments.filter((enrollment) => enrollment.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND);
93325
+ setAttribute("app.timeback.hole_filling_enrollment_count", holeFilling.length);
93326
+ if (holeFilling.length === 0) {
93327
+ return { contexts: [] };
93148
93328
  }
93149
- if (!userData.timebackId) {
93150
- throw new NotFoundError("Timeback account not found for user");
93329
+ const contexts = await Promise.all(holeFilling.map((enrollment) => this.buildHoleFillingContext(studentId, enrollment)));
93330
+ return { contexts };
93331
+ });
93332
+ }
93333
+ async buildHoleFillingContext(studentId, enrollment) {
93334
+ const client = this.requireClient();
93335
+ const grade = enrollment.grade;
93336
+ const subject = enrollment.subject;
93337
+ const enrollmentContext = {
93338
+ courseId: enrollment.courseId,
93339
+ baseCourseId: enrollment.baseCourseId ?? "",
93340
+ ...enrollment.id && { enrollmentId: enrollment.id },
93341
+ grade,
93342
+ subject,
93343
+ beginDate: enrollment.beginDate ?? null
93344
+ };
93345
+ const stampedAssignmentId = assignmentIdOf({ metadata: enrollment.enrollmentMetadata });
93346
+ let parent = null;
93347
+ if (stampedAssignmentId) {
93348
+ const where = {
93349
+ ...buildAssignmentResultWhere({
93350
+ studentId,
93351
+ assignmentId: stampedAssignmentId,
93352
+ resultType: "assessment"
93353
+ }),
93354
+ scoreStatus: SCORE_STATUS2.fullyGraded
93355
+ };
93356
+ const page = await client.api.oneroster.assessmentResults.list({
93357
+ limit: 1,
93358
+ where,
93359
+ sort: "scoreDate",
93360
+ orderBy: "desc"
93361
+ });
93362
+ parent = page.data[0] ?? null;
93363
+ }
93364
+ const summary = parent ? toTriggeringResult(parent) : null;
93365
+ let trigger = "not_stamped";
93366
+ if (stampedAssignmentId) {
93367
+ trigger = summary ? "found" : "not_found";
93368
+ }
93369
+ setAttributes({
93370
+ "app.timeback.hole_filling_trigger": trigger,
93371
+ "app.timeback.hole_filling_assignment_id": stampedAssignmentId
93372
+ });
93373
+ if (!parent || !summary) {
93374
+ return {
93375
+ enrollment: enrollmentContext,
93376
+ triggeringResult: null,
93377
+ missedStandards: [],
93378
+ untaggedMissedQuestionNumbers: []
93379
+ };
93380
+ }
93381
+ const cacheKey = `${enrollment.courseId}:${studentId}:${summary.resultId}`;
93382
+ const cached = this.holeFillingContextCache.get(cacheKey);
93383
+ if (cached && Date.now() - cached.at < TimebackService.HOLE_FILLING_CONTEXT_TTL_MS) {
93384
+ return { ...cached.context, enrollment: enrollmentContext };
93385
+ }
93386
+ const { context, complete } = await this.resolveMissedStandards(studentId, parent, summary, enrollmentContext);
93387
+ setAttribute("app.timeback.hole_filling_standards_complete", complete);
93388
+ if (complete) {
93389
+ setBounded(this.holeFillingContextCache, cacheKey, { at: Date.now(), context }, TimebackService.HOLE_FILLING_CONTEXT_CACHE_SIZE);
93390
+ }
93391
+ return context;
93392
+ }
93393
+ async resolveMissedStandards(studentId, parent, summary, enrollmentContext) {
93394
+ const client = this.requireClient();
93395
+ let complete = true;
93396
+ let missed = missedQuestionsFromInlineResponses(parent);
93397
+ if (!missed) {
93398
+ const where = buildAssignmentResultWhere({
93399
+ studentId,
93400
+ assignmentId: summary.assignmentId,
93401
+ resultType: "question"
93402
+ });
93403
+ const questionResults = await client.api.oneroster.assessmentResults.listAll({
93404
+ limit: 200,
93405
+ where,
93406
+ sort: "scoreDate",
93407
+ orderBy: "desc"
93408
+ });
93409
+ missed = missedQuestionsFromQuestionResults(questionResults);
93410
+ }
93411
+ let qtiTestId = "";
93412
+ let qtiQuestions = [];
93413
+ const standardsByItemId = new Map;
93414
+ if (missed.length > 0 && !client.hasQti) {
93415
+ complete = false;
93416
+ missed = missed.map(({ questionNumber }) => ({ questionNumber }));
93417
+ } else if (missed.some((question) => !question.qtiItemId)) {
93418
+ const lineItem = await client.api.oneroster.assessmentLineItems.get(summary.lineItemId).catch(() => null);
93419
+ qtiTestId = lineItem ? await resolveQtiTestId(client, resolveGradeLevelTestName(parent, lineItem), lineItem) : "";
93420
+ complete &&= Boolean(qtiTestId);
93421
+ if (qtiTestId) {
93422
+ qtiQuestions = await client.qtiApi.assessmentTests.getQuestions(qtiTestId).then((response) => response.questions ?? []).catch(() => {
93423
+ complete = false;
93424
+ return [];
93425
+ });
93426
+ for (const question of qtiQuestions) {
93427
+ const id = question.question.identifier || question.reference.identifier;
93428
+ if (id) {
93429
+ standardsByItemId.set(id, standardsFromQtiItem(question.question));
93430
+ }
93431
+ }
93151
93432
  }
93152
- const enrollments = await this.fetchEnrollments(userData.timebackId);
93153
- return gameId ? filterEnrollmentsByGame(enrollments, gameId) : enrollments;
93433
+ }
93434
+ const itemIdsToFetch = [
93435
+ ...new Set(missed.flatMap((question) => question.qtiItemId && !standardsByItemId.has(question.qtiItemId) ? [question.qtiItemId] : []))
93436
+ ];
93437
+ const items = await Promise.all(itemIdsToFetch.map((id) => client.qtiApi.assessmentItems.get(id).then((item) => [id, standardsFromQtiItem(item)]).catch(() => {
93438
+ complete = false;
93439
+ return [id, []];
93440
+ })));
93441
+ for (const [id, standards] of items) {
93442
+ standardsByItemId.set(id, standards);
93443
+ }
93444
+ const { missedStandards, untaggedQuestionNumbers } = groupMissedStandards(missed, standardsByItemId, qtiItemIdForQuestionNumber(qtiQuestions));
93445
+ setAttributes({
93446
+ "app.timeback.hole_filling_missed_question_count": missed.length,
93447
+ "app.timeback.hole_filling_missed_standard_count": missedStandards.length,
93448
+ "app.timeback.hole_filling_untagged_question_count": untaggedQuestionNumbers.length
93154
93449
  });
93450
+ return {
93451
+ context: {
93452
+ enrollment: enrollmentContext,
93453
+ triggeringResult: { ...summary, ...qtiTestId && { qtiTestId } },
93454
+ missedStandards,
93455
+ untaggedMissedQuestionNumbers: untaggedQuestionNumbers
93456
+ },
93457
+ complete
93458
+ };
93155
93459
  }
93156
93460
  async getUserDataByTimebackId(timebackId) {
93157
93461
  return this.withClientTelemetry(async () => {
@@ -95384,6 +95688,13 @@ var init_embed_util = __esm(() => {
95384
95688
  });
95385
95689
 
95386
95690
  // ../api-core/src/services/embed.service.ts
95691
+ async function scopedContextId(partnerId, contextId) {
95692
+ return sha256Hex(`${partnerId}:${contextId}`);
95693
+ }
95694
+ function launchTokenClaims(launch) {
95695
+ return launch.contextId ? { contextId: launch.contextId } : {};
95696
+ }
95697
+
95387
95698
  class EmbedService {
95388
95699
  deps;
95389
95700
  constructor(deps) {
@@ -95427,6 +95738,7 @@ class EmbedService {
95427
95738
  gameId: game.id,
95428
95739
  parentOrigin: input.parentOrigin,
95429
95740
  timebackCourseId: input.timebackCourseId ?? null,
95741
+ contextId: input.contextId ? await scopedContextId(partner.id, input.contextId) : null,
95430
95742
  passHash: await sha256Hex(pass),
95431
95743
  passExpiresAt,
95432
95744
  expiresAt: new Date(now + EMBED_LAUNCH_TTL_MS)
@@ -95434,7 +95746,11 @@ class EmbedService {
95434
95746
  if (!launch) {
95435
95747
  throw new InternalError("Failed to create embed launch");
95436
95748
  }
95437
- setAttributes({ "app.launch.id": launch.id, "app.user.id": user.id });
95749
+ setAttributes({
95750
+ "app.launch.id": launch.id,
95751
+ "app.user.id": user.id,
95752
+ "app.embed.has_context": input.contextId !== undefined
95753
+ });
95438
95754
  return { launchId: launch.id, pass, passExpiresAt, gameSlug: game.slug };
95439
95755
  }
95440
95756
  async redeemLaunch(input, timeZone) {
@@ -95449,7 +95765,7 @@ class EmbedService {
95449
95765
  const [game, profile, token] = await Promise.all([
95450
95766
  this.deps.getGameForStudent(launch.gameId, user),
95451
95767
  this.deps.getGameProfile(user, launch.gameId, timeZone),
95452
- this.deps.mintGameToken(launch.gameId, user.id)
95768
+ this.deps.mintGameToken(launch.gameId, user.id, launchTokenClaims(launch))
95453
95769
  ]);
95454
95770
  await this.deps.db.update(embedLaunches).set({ redeemedAt: new Date }).where(eq(embedLaunches.id, launch.id));
95455
95771
  return {
@@ -95481,7 +95797,7 @@ class EmbedService {
95481
95797
  if (outcome) {
95482
95798
  throw new AccessDeniedError("This launch has ended");
95483
95799
  }
95484
- return this.deps.mintGameToken(launch.gameId, launch.userId);
95800
+ return this.deps.mintGameToken(launch.gameId, launch.userId, launchTokenClaims(launch));
95485
95801
  }
95486
95802
  async endLaunch(launchId) {
95487
95803
  await this.deps.db.update(embedLaunches).set({ endedAt: sql`coalesce(${embedLaunches.endedAt}, now())`, passHash: null }).where(eq(embedLaunches.id, launchId));
@@ -96583,10 +96899,10 @@ function createSandboxAuthProvider() {
96583
96899
  }
96584
96900
  return deleted;
96585
96901
  },
96586
- async mintGameToken(gameId, userId) {
96902
+ async mintGameToken(gameId, userId, claims) {
96587
96903
  const header = btoa(JSON.stringify({ alg: "none", typ: "sandbox" }));
96588
96904
  const exp = Math.floor(Date.now() / 1000) + 15 * 60;
96589
- const payload = btoa(JSON.stringify({ sub: gameId, uid: userId, exp }));
96905
+ const payload = btoa(JSON.stringify({ ...claims, sub: gameId, uid: userId, exp }));
96590
96906
  const token = `${header}.${payload}.sandbox`;
96591
96907
  return { token, exp: exp * 1000 };
96592
96908
  },
@@ -96711,33 +97027,33 @@ var init_auth_provider = __esm(async () => {
96711
97027
  function createSandboxCacheProvider() {
96712
97028
  return {
96713
97029
  async get(key) {
96714
- const entry = cache.get(key);
97030
+ const entry = cache2.get(key);
96715
97031
  if (!entry) {
96716
97032
  return null;
96717
97033
  }
96718
97034
  if (entry.expiresAt < Date.now()) {
96719
- cache.delete(key);
97035
+ cache2.delete(key);
96720
97036
  return null;
96721
97037
  }
96722
97038
  return entry.value;
96723
97039
  },
96724
97040
  async set(key, value, ttlSeconds = 300) {
96725
- cache.set(key, {
97041
+ cache2.set(key, {
96726
97042
  value,
96727
97043
  expiresAt: Date.now() + ttlSeconds * 1000
96728
97044
  });
96729
97045
  },
96730
97046
  async delete(key) {
96731
- cache.delete(key);
97047
+ cache2.delete(key);
96732
97048
  }
96733
97049
  };
96734
97050
  }
96735
97051
  function clearSandboxCache() {
96736
- cache.clear();
97052
+ cache2.clear();
96737
97053
  }
96738
- var cache;
97054
+ var cache2;
96739
97055
  var init_cache_provider = __esm(() => {
96740
- cache = new Map;
97056
+ cache2 = new Map;
96741
97057
  });
96742
97058
 
96743
97059
  // ../utils/src/keys.ts
@@ -152965,7 +153281,7 @@ function parseQtiLibraryParams(searchParams) {
152965
153281
  limit
152966
153282
  };
152967
153283
  }
152968
- var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, getHoleFillingCourses, createHoleFillingCourse, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, prepareRuntimeAssessment, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, resumeRuntimeAssessment, saveRuntimeAssessment, recordRuntimeAssessmentProgress, submitRuntimeAssessmentItem, submitRuntimeAssessment, finalizeRuntimeAssessment, exportRuntimeAssessmentFixture, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getGradeLevelTestResults, getGradeLevelTestReview, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, attachExistingAssessments, updateAssessment, reviseAssessment, synchronizeReviewBank, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
153284
+ var populateStudent, getUser, getUserEnrollments, getUserHoleFillingContext, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, getHoleFillingCourses, createHoleFillingCourse, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, prepareRuntimeAssessment, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, resumeRuntimeAssessment, saveRuntimeAssessment, recordRuntimeAssessmentProgress, submitRuntimeAssessmentItem, submitRuntimeAssessment, finalizeRuntimeAssessment, exportRuntimeAssessmentFixture, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getGradeLevelTestResults, getGradeLevelTestReview, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, attachExistingAssessments, updateAssessment, reviseAssessment, synchronizeReviewBank, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
152969
153285
  var init_timeback_controller = __esm(() => {
152970
153286
  init_esm();
152971
153287
  init_src();
@@ -152991,6 +153307,12 @@ var init_timeback_controller = __esm(() => {
152991
153307
  });
152992
153308
  getUser = requireNonAnonymous(async (ctx) => ctx.services.timeback.getUserData(ctx.user.id, ctx.gameId));
152993
153309
  getUserEnrollments = requireNonAnonymous(async (ctx) => ctx.services.timeback.getUserEnrollments(ctx.user.id, ctx.gameId));
153310
+ getUserHoleFillingContext = requireNonAnonymous(async (ctx) => {
153311
+ if (!ctx.gameId) {
153312
+ throw ApiError.forbidden("Hole-filling context requires a game token");
153313
+ }
153314
+ return ctx.services.timeback.getHoleFillingContext(ctx.user.id, ctx.gameId);
153315
+ });
152994
153316
  getUserById = requireNonAnonymous(async (ctx) => {
152995
153317
  const timebackId = ctx.params.timebackId;
152996
153318
  if (!timebackId) {
@@ -153844,6 +154166,7 @@ var init_timeback_controller = __esm(() => {
153844
154166
  populateStudent,
153845
154167
  getUser,
153846
154168
  getUserEnrollments,
154169
+ getUserHoleFillingContext,
153847
154170
  getUserById,
153848
154171
  setupIntegration,
153849
154172
  getIntegrations,
@@ -154906,6 +155229,12 @@ var init_timeback8 = __esm(async () => {
154906
155229
  }
154907
155230
  return handle2(timeback2.getUserEnrollments)(c);
154908
155231
  });
155232
+ timebackRouter.get("/user/hole-filling-context", async (c) => {
155233
+ if (shouldMockTimeback()) {
155234
+ return c.json({ contexts: [] });
155235
+ }
155236
+ return handle2(timeback2.getUserHoleFillingContext)(c);
155237
+ });
154909
155238
  timebackRouter.get("/user/:timebackId", async (c) => {
154910
155239
  const user = c.get("user");
154911
155240
  if (!user) {