@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.
- package/dist/cli.js +428 -99
- package/dist/server.js +428 -99
- 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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
93143
|
-
|
|
93144
|
-
|
|
93145
|
-
|
|
93146
|
-
|
|
93147
|
-
|
|
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
|
-
|
|
93150
|
-
|
|
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
|
-
|
|
93153
|
-
|
|
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({
|
|
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 =
|
|
97030
|
+
const entry = cache2.get(key);
|
|
96715
97031
|
if (!entry) {
|
|
96716
97032
|
return null;
|
|
96717
97033
|
}
|
|
96718
97034
|
if (entry.expiresAt < Date.now()) {
|
|
96719
|
-
|
|
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
|
-
|
|
97041
|
+
cache2.set(key, {
|
|
96726
97042
|
value,
|
|
96727
97043
|
expiresAt: Date.now() + ttlSeconds * 1000
|
|
96728
97044
|
});
|
|
96729
97045
|
},
|
|
96730
97046
|
async delete(key) {
|
|
96731
|
-
|
|
97047
|
+
cache2.delete(key);
|
|
96732
97048
|
}
|
|
96733
97049
|
};
|
|
96734
97050
|
}
|
|
96735
97051
|
function clearSandboxCache() {
|
|
96736
|
-
|
|
97052
|
+
cache2.clear();
|
|
96737
97053
|
}
|
|
96738
|
-
var
|
|
97054
|
+
var cache2;
|
|
96739
97055
|
var init_cache_provider = __esm(() => {
|
|
96740
|
-
|
|
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) {
|