@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/server.js
CHANGED
|
@@ -697,7 +697,7 @@ var package_default;
|
|
|
697
697
|
var init_package = __esm(() => {
|
|
698
698
|
package_default = {
|
|
699
699
|
name: "@playcademy/sandbox",
|
|
700
|
-
version: "0.12.2-beta.
|
|
700
|
+
version: "0.12.2-beta.5",
|
|
701
701
|
description: "Local development server for Playcademy game development",
|
|
702
702
|
type: "module",
|
|
703
703
|
exports: {
|
|
@@ -19097,6 +19097,7 @@ var init_table8 = __esm(() => {
|
|
|
19097
19097
|
gameId: uuid("game_id").notNull().references(() => games.id, { onDelete: "cascade" }),
|
|
19098
19098
|
parentOrigin: text("parent_origin").notNull(),
|
|
19099
19099
|
timebackCourseId: text("timeback_course_id"),
|
|
19100
|
+
contextId: text("context_id"),
|
|
19100
19101
|
passHash: text("pass_hash").unique(),
|
|
19101
19102
|
passExpiresAt: timestamp("pass_expires_at", { withTimezone: true }).notNull(),
|
|
19102
19103
|
redeemedAt: timestamp("redeemed_at", { withTimezone: true }),
|
|
@@ -49003,13 +49004,16 @@ function isCanonicalOrigin(value) {
|
|
|
49003
49004
|
}
|
|
49004
49005
|
return url.protocol === "https:" || url.protocol === "http:" && isLocalHostname(url.hostname);
|
|
49005
49006
|
}
|
|
49006
|
-
var LOCAL_HOSTNAMES, EmbedOriginSchema, EmbedPartnerClientIdSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
|
|
49007
|
+
var LOCAL_HOSTNAMES, EmbedOriginSchema, EmbedContextIdSchema, EmbedPartnerClientIdSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
|
|
49007
49008
|
var init_schemas5 = __esm(() => {
|
|
49008
49009
|
init_esm();
|
|
49009
49010
|
LOCAL_HOSTNAMES = new Set(["localhost", "127.0.0.1"]);
|
|
49010
49011
|
EmbedOriginSchema = stringType().refine(isCanonicalOrigin, {
|
|
49011
49012
|
message: "Origin must be a bare https origin like https://example.com, with no path or slash"
|
|
49012
49013
|
});
|
|
49014
|
+
EmbedContextIdSchema = stringType().regex(/^[A-Za-z0-9._~-]{1,128}$/, {
|
|
49015
|
+
message: "contextId must be 1-128 URL-safe characters (letters, digits, . _ ~ -)"
|
|
49016
|
+
});
|
|
49013
49017
|
EmbedPartnerClientIdSchema = stringType().trim().min(1).max(200);
|
|
49014
49018
|
EnsureEmbedPartnerSchema = objectType({
|
|
49015
49019
|
name: stringType().trim().min(1).max(100)
|
|
@@ -49021,7 +49025,8 @@ var init_schemas5 = __esm(() => {
|
|
|
49021
49025
|
timebackId: stringType().trim().min(1).max(200),
|
|
49022
49026
|
gameSlug: stringType().trim().min(1).max(100),
|
|
49023
49027
|
parentOrigin: EmbedOriginSchema,
|
|
49024
|
-
timebackCourseId: stringType().trim().min(1).max(200).optional()
|
|
49028
|
+
timebackCourseId: stringType().trim().min(1).max(200).optional(),
|
|
49029
|
+
contextId: EmbedContextIdSchema.optional()
|
|
49025
49030
|
}).strict();
|
|
49026
49031
|
RedeemEmbedPassSchema = objectType({
|
|
49027
49032
|
pass: stringType().min(1).max(200),
|
|
@@ -80045,6 +80050,9 @@ function normalizedIdentityCase2(value) {
|
|
|
80045
80050
|
function frameworkAliasKey2(value) {
|
|
80046
80051
|
return value.normalize("NFKC").toLocaleLowerCase("en-US").replace(/[^a-z0-9]+/g, "");
|
|
80047
80052
|
}
|
|
80053
|
+
function isUuidShaped2(value) {
|
|
80054
|
+
return UUID_PATTERN2.test(value.trim());
|
|
80055
|
+
}
|
|
80048
80056
|
function isCommonCoreMathIdentifier2(identifier) {
|
|
80049
80057
|
const canonical = normalizedIdentityCase2(normalizedWhitespace2(identifier));
|
|
80050
80058
|
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);
|
|
@@ -80076,6 +80084,14 @@ function canonicalIdentifier2(framework, authoredIdentifier) {
|
|
|
80076
80084
|
}
|
|
80077
80085
|
return identifier;
|
|
80078
80086
|
}
|
|
80087
|
+
function readableAlignmentIdentifier2(alignment) {
|
|
80088
|
+
const identifier = alignment.identifier?.trim() ?? "";
|
|
80089
|
+
if (identifier && !isUuidShaped2(identifier)) {
|
|
80090
|
+
return identifier;
|
|
80091
|
+
}
|
|
80092
|
+
const id = alignment.id.trim();
|
|
80093
|
+
return id && !isUuidShaped2(id) ? id : "";
|
|
80094
|
+
}
|
|
80079
80095
|
function canonicalAssessmentStandardRef2(standard) {
|
|
80080
80096
|
const authoredFramework = normalizedWhitespace2(standard.framework);
|
|
80081
80097
|
const framework = canonicalFramework2(authoredFramework, standard.identifier);
|
|
@@ -80182,7 +80198,23 @@ function qtiStandardsFromMetadata(metadata2) {
|
|
|
80182
80198
|
standards.push(...qtiAlignmentStandardsFromMetadata2(metadata2));
|
|
80183
80199
|
return dedupeStandards2(standards);
|
|
80184
80200
|
}
|
|
80185
|
-
|
|
80201
|
+
function qtiAssessmentStandardRefsFromMetadata2(metadata2) {
|
|
80202
|
+
const refs = new Map;
|
|
80203
|
+
for (const alignment of qtiAlignmentStandardsFromMetadata2(metadata2)) {
|
|
80204
|
+
const identifier = readableAlignmentIdentifier2(alignment);
|
|
80205
|
+
if (identifier) {
|
|
80206
|
+
const canonical = canonicalAssessmentStandardRef2({
|
|
80207
|
+
framework: alignment.source,
|
|
80208
|
+
identifier
|
|
80209
|
+
});
|
|
80210
|
+
if (canonical.framework && canonical.identifier) {
|
|
80211
|
+
refs.set(assessmentStandardRefKey2(canonical), canonical);
|
|
80212
|
+
}
|
|
80213
|
+
}
|
|
80214
|
+
}
|
|
80215
|
+
return [...refs.values()];
|
|
80216
|
+
}
|
|
80217
|
+
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;
|
|
80186
80218
|
var init_qti = __esm(() => {
|
|
80187
80219
|
init_timeback4();
|
|
80188
80220
|
init_timeback4();
|
|
@@ -80544,6 +80576,7 @@ var init_qti = __esm(() => {
|
|
|
80544
80576
|
this.name = "QtiScoringArtifactValidationError";
|
|
80545
80577
|
}
|
|
80546
80578
|
};
|
|
80579
|
+
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;
|
|
80547
80580
|
COMMON_CORE_MATH_FRAMEWORK_ALIASES2 = new Set([
|
|
80548
80581
|
"ccssm",
|
|
80549
80582
|
"ccssmath",
|
|
@@ -80753,7 +80786,7 @@ function pruneUnreadableStandards(standards) {
|
|
|
80753
80786
|
return standards.filter(hasReadableStandardLabel);
|
|
80754
80787
|
}
|
|
80755
80788
|
function parseQuestionIndex(title) {
|
|
80756
|
-
const match = title
|
|
80789
|
+
const match = title?.match(/\bquestion[\s_-]*(\d+)\b/i);
|
|
80757
80790
|
const index = match ? Number(match[1]) : Number.NaN;
|
|
80758
80791
|
return Number.isFinite(index) && index > 0 ? index : null;
|
|
80759
80792
|
}
|
|
@@ -80787,14 +80820,15 @@ function qtiQuestionsByIndex(qtiQuestions) {
|
|
|
80787
80820
|
}
|
|
80788
80821
|
return questionsByIndex;
|
|
80789
80822
|
}
|
|
80823
|
+
function normalizedQuestionScore(score, scorePercentile) {
|
|
80824
|
+
const rawValue = numberField(score) ?? numberField(scorePercentile);
|
|
80825
|
+
return rawValue !== null && rawValue > 1 ? rawValue / 100 : rawValue;
|
|
80826
|
+
}
|
|
80790
80827
|
function getQuestionCorrectness(result) {
|
|
80791
80828
|
if (!result) {
|
|
80792
80829
|
return "unknown";
|
|
80793
80830
|
}
|
|
80794
|
-
const
|
|
80795
|
-
const scorePercentile = numberField(result.scorePercentile);
|
|
80796
|
-
const rawValue = score ?? scorePercentile;
|
|
80797
|
-
const value = rawValue !== null && rawValue > 1 ? rawValue / 100 : rawValue;
|
|
80831
|
+
const value = normalizedQuestionScore(result.score, result.scorePercentile);
|
|
80798
80832
|
if (value === null) {
|
|
80799
80833
|
return "unknown";
|
|
80800
80834
|
}
|
|
@@ -81058,10 +81092,71 @@ var init_timeback_mastery_completion_util = __esm(async () => {
|
|
|
81058
81092
|
await init_errors8();
|
|
81059
81093
|
});
|
|
81060
81094
|
|
|
81095
|
+
// ../api-core/src/utils/bounded-map.util.ts
|
|
81096
|
+
function setBounded(map, key, value, maxSize) {
|
|
81097
|
+
map.delete(key);
|
|
81098
|
+
if (map.size >= maxSize) {
|
|
81099
|
+
const oldestKey = map.keys().next().value;
|
|
81100
|
+
if (oldestKey !== undefined) {
|
|
81101
|
+
map.delete(oldestKey);
|
|
81102
|
+
}
|
|
81103
|
+
}
|
|
81104
|
+
map.set(key, value);
|
|
81105
|
+
}
|
|
81106
|
+
|
|
81107
|
+
// ../api-core/src/utils/timeback-qti-test-lookup.util.ts
|
|
81108
|
+
async function resolveQtiTestId(client, testName, parentLineItem) {
|
|
81109
|
+
const cacheKey = `${parentLineItem.sourcedId ?? ""}:${testName}`;
|
|
81110
|
+
const cached = cache.get(cacheKey);
|
|
81111
|
+
if (cached) {
|
|
81112
|
+
setBounded(cache, cacheKey, cached, MAX_CACHE_SIZE);
|
|
81113
|
+
return cached;
|
|
81114
|
+
}
|
|
81115
|
+
let resource = null;
|
|
81116
|
+
const resourceId = sourceIdFromRef(parentLineItem.componentResource);
|
|
81117
|
+
if (resourceId) {
|
|
81118
|
+
resource = await client.api.oneroster.resources.get(resourceId).catch(() => null);
|
|
81119
|
+
}
|
|
81120
|
+
for (const candidate of qtiIdCandidates(parentLineItem, resource)) {
|
|
81121
|
+
const test = await client.qtiApi.assessmentTests.get(candidate).catch(() => null);
|
|
81122
|
+
if (test?.identifier) {
|
|
81123
|
+
setBounded(cache, cacheKey, test.identifier, MAX_CACHE_SIZE);
|
|
81124
|
+
return test.identifier;
|
|
81125
|
+
}
|
|
81126
|
+
}
|
|
81127
|
+
const normalizedTestName = testName.trim();
|
|
81128
|
+
if (!normalizedTestName) {
|
|
81129
|
+
return "";
|
|
81130
|
+
}
|
|
81131
|
+
for (let page = 1;page <= TITLE_LOOKUP_MAX_PAGES; page++) {
|
|
81132
|
+
const list = await client.qtiApi.assessmentTests.list({
|
|
81133
|
+
query: normalizedTestName,
|
|
81134
|
+
limit: TITLE_LOOKUP_LIMIT,
|
|
81135
|
+
page,
|
|
81136
|
+
sort: "title",
|
|
81137
|
+
order: "asc"
|
|
81138
|
+
}).catch(() => null);
|
|
81139
|
+
const qtiTestId = findQtiAssessmentTestIdByTitle(list?.items ?? [], normalizedTestName);
|
|
81140
|
+
if (qtiTestId) {
|
|
81141
|
+
setBounded(cache, cacheKey, qtiTestId, MAX_CACHE_SIZE);
|
|
81142
|
+
return qtiTestId;
|
|
81143
|
+
}
|
|
81144
|
+
const itemCount = list?.items?.length ?? 0;
|
|
81145
|
+
if (!list || itemCount < TITLE_LOOKUP_LIMIT || typeof list.pages === "number" && page >= list.pages) {
|
|
81146
|
+
break;
|
|
81147
|
+
}
|
|
81148
|
+
}
|
|
81149
|
+
return "";
|
|
81150
|
+
}
|
|
81151
|
+
var MAX_CACHE_SIZE = 500, TITLE_LOOKUP_LIMIT = 50, TITLE_LOOKUP_MAX_PAGES = 5, cache;
|
|
81152
|
+
var init_timeback_qti_test_lookup_util = __esm(() => {
|
|
81153
|
+
init_timeback_grade_level_results_util();
|
|
81154
|
+
cache = new Map;
|
|
81155
|
+
});
|
|
81156
|
+
|
|
81061
81157
|
// ../api-core/src/services/timeback-admin.service.ts
|
|
81062
81158
|
class TimebackAdminService {
|
|
81063
81159
|
deps;
|
|
81064
|
-
qtiTestIdCache = new Map;
|
|
81065
81160
|
static XP_PRECISION_FACTOR = 10;
|
|
81066
81161
|
static RECENT_ACTIVITY_LIMIT = 20;
|
|
81067
81162
|
static MAX_STUDENT_ACTIVITY_LIMIT = 200;
|
|
@@ -81076,9 +81171,6 @@ class TimebackAdminService {
|
|
|
81076
81171
|
static GAME_METRICS_FETCH_TIMEOUT_MS = 1e4;
|
|
81077
81172
|
static GAME_METRICS_LIST_FETCH_TIMEOUT_MS = 3000;
|
|
81078
81173
|
static GAME_METRICS_RUN_IDS_PER_REQUEST = 50;
|
|
81079
|
-
static MAX_QTI_TEST_ID_CACHE_SIZE = 500;
|
|
81080
|
-
static QTI_TEST_TITLE_LOOKUP_LIMIT = 50;
|
|
81081
|
-
static QTI_TEST_TITLE_LOOKUP_MAX_PAGES = 5;
|
|
81082
81174
|
constructor(deps) {
|
|
81083
81175
|
this.deps = deps;
|
|
81084
81176
|
}
|
|
@@ -81938,68 +82030,6 @@ class TimebackAdminService {
|
|
|
81938
82030
|
return { activities: activitiesWithGameMetrics, hasMore };
|
|
81939
82031
|
});
|
|
81940
82032
|
}
|
|
81941
|
-
rememberQtiTestId(cacheKey, qtiTestId) {
|
|
81942
|
-
if (!qtiTestId) {
|
|
81943
|
-
this.qtiTestIdCache.delete(cacheKey);
|
|
81944
|
-
return;
|
|
81945
|
-
}
|
|
81946
|
-
if (this.qtiTestIdCache.has(cacheKey)) {
|
|
81947
|
-
this.qtiTestIdCache.delete(cacheKey);
|
|
81948
|
-
} else if (this.qtiTestIdCache.size >= TimebackAdminService.MAX_QTI_TEST_ID_CACHE_SIZE) {
|
|
81949
|
-
const oldestKey = this.qtiTestIdCache.keys().next().value;
|
|
81950
|
-
if (oldestKey !== undefined) {
|
|
81951
|
-
this.qtiTestIdCache.delete(oldestKey);
|
|
81952
|
-
}
|
|
81953
|
-
}
|
|
81954
|
-
this.qtiTestIdCache.set(cacheKey, qtiTestId);
|
|
81955
|
-
}
|
|
81956
|
-
async resolveQtiTestIdForGradeLevelResult(client, testName, parentLineItem) {
|
|
81957
|
-
const cacheKey = `${parentLineItem.sourcedId ?? ""}:${testName}`;
|
|
81958
|
-
const cachedQtiTestId = cacheKey ? this.qtiTestIdCache.get(cacheKey) : undefined;
|
|
81959
|
-
if (cachedQtiTestId) {
|
|
81960
|
-
this.rememberQtiTestId(cacheKey, cachedQtiTestId);
|
|
81961
|
-
return cachedQtiTestId;
|
|
81962
|
-
}
|
|
81963
|
-
let resource = null;
|
|
81964
|
-
const resourceId = sourceIdFromRef(parentLineItem.componentResource);
|
|
81965
|
-
if (resourceId) {
|
|
81966
|
-
resource = await client.api.oneroster.resources.get(resourceId).catch(() => null);
|
|
81967
|
-
}
|
|
81968
|
-
for (const candidate of qtiIdCandidates(parentLineItem, resource)) {
|
|
81969
|
-
const test = await client.qtiApi.assessmentTests.get(candidate).catch(() => null);
|
|
81970
|
-
if (test?.identifier) {
|
|
81971
|
-
if (cacheKey) {
|
|
81972
|
-
this.rememberQtiTestId(cacheKey, test.identifier);
|
|
81973
|
-
}
|
|
81974
|
-
return test.identifier;
|
|
81975
|
-
}
|
|
81976
|
-
}
|
|
81977
|
-
const normalizedTestName = testName.trim();
|
|
81978
|
-
if (!normalizedTestName) {
|
|
81979
|
-
return "";
|
|
81980
|
-
}
|
|
81981
|
-
for (let page = 1;page <= TimebackAdminService.QTI_TEST_TITLE_LOOKUP_MAX_PAGES; page++) {
|
|
81982
|
-
const list = await client.qtiApi.assessmentTests.list({
|
|
81983
|
-
query: normalizedTestName,
|
|
81984
|
-
limit: TimebackAdminService.QTI_TEST_TITLE_LOOKUP_LIMIT,
|
|
81985
|
-
page,
|
|
81986
|
-
sort: "title",
|
|
81987
|
-
order: "asc"
|
|
81988
|
-
}).catch(() => null);
|
|
81989
|
-
const qtiTestId = findQtiAssessmentTestIdByTitle(list?.items ?? [], normalizedTestName);
|
|
81990
|
-
if (qtiTestId) {
|
|
81991
|
-
if (cacheKey) {
|
|
81992
|
-
this.rememberQtiTestId(cacheKey, qtiTestId);
|
|
81993
|
-
}
|
|
81994
|
-
return qtiTestId;
|
|
81995
|
-
}
|
|
81996
|
-
const itemCount = list?.items?.length ?? 0;
|
|
81997
|
-
if (!list || itemCount < TimebackAdminService.QTI_TEST_TITLE_LOOKUP_LIMIT || typeof list.pages === "number" && page >= list.pages) {
|
|
81998
|
-
break;
|
|
81999
|
-
}
|
|
82000
|
-
}
|
|
82001
|
-
return "";
|
|
82002
|
-
}
|
|
82003
82033
|
async getGradeLevelTestResults(user, options) {
|
|
82004
82034
|
return this.withClientTelemetry(async () => {
|
|
82005
82035
|
const { gameId, courseId, studentId } = options;
|
|
@@ -82102,7 +82132,7 @@ class TimebackAdminService {
|
|
|
82102
82132
|
}).then((page) => page.data) : [];
|
|
82103
82133
|
const latestResultsByLineItem = latestAssessmentResultsByLineItem(questionResults);
|
|
82104
82134
|
const testName = resolveGradeLevelTestName(parentResult, parentLineItem);
|
|
82105
|
-
const qtiTestId = await
|
|
82135
|
+
const qtiTestId = await resolveQtiTestId(client, testName, parentLineItem);
|
|
82106
82136
|
const qtiQuestions = qtiTestId ? await client.qtiApi.assessmentTests.getQuestions(qtiTestId).then((response) => response.questions ?? []).catch(() => []) : [];
|
|
82107
82137
|
const questions = mapGradeLevelReviewQuestions({
|
|
82108
82138
|
questionLineItems,
|
|
@@ -82796,6 +82826,7 @@ var init_timeback_admin_service = __esm(async () => {
|
|
|
82796
82826
|
init_timeback_discrepancy_queue_util();
|
|
82797
82827
|
init_timeback_game_metrics_comparison_util();
|
|
82798
82828
|
init_timeback_grade_level_results_util();
|
|
82829
|
+
init_timeback_qti_test_lookup_util();
|
|
82799
82830
|
init_timeback_util();
|
|
82800
82831
|
await __promiseAll([
|
|
82801
82832
|
init_dist5(),
|
|
@@ -90802,6 +90833,128 @@ var init_timeback_create_integration_util = __esm(() => {
|
|
|
90802
90833
|
init_timeback_util();
|
|
90803
90834
|
});
|
|
90804
90835
|
|
|
90836
|
+
// ../api-core/src/utils/timeback-hole-filling-context.util.ts
|
|
90837
|
+
function buildAssignmentResultWhere(options) {
|
|
90838
|
+
return {
|
|
90839
|
+
status: "active",
|
|
90840
|
+
"student.sourcedId": options.studentId,
|
|
90841
|
+
"metadata.resultType": options.resultType,
|
|
90842
|
+
"metadata.assignmentId": options.assignmentId
|
|
90843
|
+
};
|
|
90844
|
+
}
|
|
90845
|
+
function assignmentIdOf(record) {
|
|
90846
|
+
const value = metadataOf(record).assignmentId;
|
|
90847
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
90848
|
+
return String(value);
|
|
90849
|
+
}
|
|
90850
|
+
const text = stringField3(value).trim();
|
|
90851
|
+
return text || undefined;
|
|
90852
|
+
}
|
|
90853
|
+
function toTriggeringResult(result) {
|
|
90854
|
+
const metadata2 = metadataOf(result);
|
|
90855
|
+
const testType = normalizeTestType(metadata2.testType);
|
|
90856
|
+
const assignmentId = assignmentIdOf(result);
|
|
90857
|
+
const lineItemId = sourceIdFromRef(result.assessmentLineItem);
|
|
90858
|
+
if (!assignmentId || !lineItemId || result.scoreStatus !== SCORE_STATUS2.fullyGraded || !isAllowedGradeLevelTestType(testType)) {
|
|
90859
|
+
return null;
|
|
90860
|
+
}
|
|
90861
|
+
return {
|
|
90862
|
+
resultId: result.sourcedId ?? "",
|
|
90863
|
+
assignmentId,
|
|
90864
|
+
lineItemId,
|
|
90865
|
+
testName: stringField3(metadata2.testName) || lineItemId,
|
|
90866
|
+
testType,
|
|
90867
|
+
score: numberField(result.score),
|
|
90868
|
+
scoreDate: result.scoreDate ?? null,
|
|
90869
|
+
totalQuestions: numberField(metadata2.totalQuestions),
|
|
90870
|
+
correctQuestions: numberField(metadata2.correctQuestions)
|
|
90871
|
+
};
|
|
90872
|
+
}
|
|
90873
|
+
function isMissed(resultText, score, scorePercentile) {
|
|
90874
|
+
const marking = stringField3(resultText).trim().toLowerCase();
|
|
90875
|
+
if (marking) {
|
|
90876
|
+
return marking !== "correct";
|
|
90877
|
+
}
|
|
90878
|
+
const value = normalizedQuestionScore(score, scorePercentile);
|
|
90879
|
+
return value !== null && value < 1;
|
|
90880
|
+
}
|
|
90881
|
+
function missedQuestionsFromInlineResponses(result) {
|
|
90882
|
+
const responses = metadataOf(result).responses;
|
|
90883
|
+
const questions = isRecord6(responses) && Array.isArray(responses.questions) ? responses.questions : undefined;
|
|
90884
|
+
if (!questions?.length) {
|
|
90885
|
+
return;
|
|
90886
|
+
}
|
|
90887
|
+
return questions.flatMap((question) => {
|
|
90888
|
+
if (!isRecord6(question) || !isMissed(question.result, question.score)) {
|
|
90889
|
+
return [];
|
|
90890
|
+
}
|
|
90891
|
+
const questionNumber = numberField(question.questionNumber);
|
|
90892
|
+
const qtiItemId = stringField3(question.questionQtiIdentifier).trim();
|
|
90893
|
+
return questionNumber === null ? [] : [{ questionNumber, ...qtiItemId && { qtiItemId } }];
|
|
90894
|
+
});
|
|
90895
|
+
}
|
|
90896
|
+
function questionNumberFromLineItemId(lineItemId) {
|
|
90897
|
+
const match = lineItemId.match(/_q(\d+)$/);
|
|
90898
|
+
const value = match ? Number(match[1]) : Number.NaN;
|
|
90899
|
+
return Number.isInteger(value) && value > 0 ? value : null;
|
|
90900
|
+
}
|
|
90901
|
+
function missedQuestionsFromQuestionResults(questionResults) {
|
|
90902
|
+
return [...latestAssessmentResultsByLineItem(questionResults)].flatMap(([lineItemId, result]) => {
|
|
90903
|
+
const questionNumber = questionNumberFromLineItemId(lineItemId);
|
|
90904
|
+
const responses = metadataOf(result).responses;
|
|
90905
|
+
const response = isRecord6(responses) && isRecord6(responses.response) ? responses.response : undefined;
|
|
90906
|
+
if (questionNumber === null || !isMissed(response?.result, result.score, result.scorePercentile)) {
|
|
90907
|
+
return [];
|
|
90908
|
+
}
|
|
90909
|
+
const qtiItemId = stringField3(response?.questionQtiIdentifier).trim();
|
|
90910
|
+
return [{ questionNumber, ...qtiItemId && { qtiItemId } }];
|
|
90911
|
+
}).toSorted((a, b) => a.questionNumber - b.questionNumber);
|
|
90912
|
+
}
|
|
90913
|
+
function qtiItemIdForQuestionNumber(qtiQuestions) {
|
|
90914
|
+
const byNumber = qtiQuestionsByIndex(qtiQuestions);
|
|
90915
|
+
return (questionNumber) => {
|
|
90916
|
+
const question = byNumber.get(questionNumber) ?? qtiQuestions[questionNumber - 1];
|
|
90917
|
+
return question ? question.question.identifier || question.reference.identifier : undefined;
|
|
90918
|
+
};
|
|
90919
|
+
}
|
|
90920
|
+
function groupMissedStandards(missed, standardsByItemId, itemIdFor) {
|
|
90921
|
+
const byKey = new Map;
|
|
90922
|
+
const untagged = [];
|
|
90923
|
+
for (const question of missed) {
|
|
90924
|
+
const itemId = question.qtiItemId ?? itemIdFor(question.questionNumber);
|
|
90925
|
+
const standards = itemId ? standardsByItemId.get(itemId) ?? [] : [];
|
|
90926
|
+
if (!standards.length) {
|
|
90927
|
+
untagged.push(question.questionNumber);
|
|
90928
|
+
}
|
|
90929
|
+
for (const standard of standards) {
|
|
90930
|
+
const key = `${standard.framework}\x00${standard.identifier}`;
|
|
90931
|
+
const entry = byKey.get(key) ?? {
|
|
90932
|
+
framework: standard.framework,
|
|
90933
|
+
code: standard.identifier,
|
|
90934
|
+
questionNumbers: [],
|
|
90935
|
+
qtiItemIds: []
|
|
90936
|
+
};
|
|
90937
|
+
entry.questionNumbers.push(question.questionNumber);
|
|
90938
|
+
if (itemId && !entry.qtiItemIds.includes(itemId)) {
|
|
90939
|
+
entry.qtiItemIds.push(itemId);
|
|
90940
|
+
}
|
|
90941
|
+
byKey.set(key, entry);
|
|
90942
|
+
}
|
|
90943
|
+
}
|
|
90944
|
+
return {
|
|
90945
|
+
missedStandards: [...byKey.values()].toSorted((a, b) => b.questionNumbers.length - a.questionNumbers.length || a.code.localeCompare(b.code, undefined, { numeric: true })),
|
|
90946
|
+
untaggedQuestionNumbers: untagged.toSorted((a, b) => a - b)
|
|
90947
|
+
};
|
|
90948
|
+
}
|
|
90949
|
+
function standardsFromQtiItem(item) {
|
|
90950
|
+
return qtiAssessmentStandardRefsFromMetadata2(metadataOf(item));
|
|
90951
|
+
}
|
|
90952
|
+
var init_timeback_hole_filling_context_util = __esm(() => {
|
|
90953
|
+
init_constants2();
|
|
90954
|
+
init_qti();
|
|
90955
|
+
init_timeback_grade_level_results_util();
|
|
90956
|
+
});
|
|
90957
|
+
|
|
90805
90958
|
// ../api-core/src/utils/timeback-promotion.util.ts
|
|
90806
90959
|
async function promoteCompletedCourse({
|
|
90807
90960
|
db,
|
|
@@ -90931,14 +91084,20 @@ var init_timeback_service = __esm(async () => {
|
|
|
90931
91084
|
init_helpers_index();
|
|
90932
91085
|
init_tables_index();
|
|
90933
91086
|
init_spans();
|
|
91087
|
+
init_constants2();
|
|
90934
91088
|
init_types2();
|
|
90935
91089
|
init_errors2();
|
|
90936
91090
|
init_timeback_create_integration_util();
|
|
91091
|
+
init_timeback_grade_level_results_util();
|
|
91092
|
+
init_timeback_hole_filling_context_util();
|
|
90937
91093
|
init_timeback_promotion_util();
|
|
91094
|
+
init_timeback_qti_test_lookup_util();
|
|
90938
91095
|
init_timeback_removed_integration_util();
|
|
90939
91096
|
init_timeback_util();
|
|
90940
91097
|
await init_dist5();
|
|
90941
91098
|
TimebackService = class TimebackService {
|
|
91099
|
+
static HOLE_FILLING_CONTEXT_TTL_MS = 10 * 60 * 1000;
|
|
91100
|
+
static HOLE_FILLING_CONTEXT_CACHE_SIZE = 1000;
|
|
90942
91101
|
static HEARTBEAT_DEDUPE_TTL_MS = 5 * 60 * 1000;
|
|
90943
91102
|
static processedHeartbeatWindows = new Map;
|
|
90944
91103
|
static inFlightHeartbeatWindows = new Map;
|
|
@@ -90946,6 +91105,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
90946
91105
|
static COMPLETION_IN_FLIGHT_SETTLE_DELAY_MS = 400;
|
|
90947
91106
|
static COMPLETION_IN_FLIGHT_SETTLE_ATTEMPTS = 5;
|
|
90948
91107
|
deps;
|
|
91108
|
+
holeFillingContextCache = new Map;
|
|
90949
91109
|
static cleanHeartbeatDedupeCache(now = Date.now()) {
|
|
90950
91110
|
for (const [key, timestamp] of this.processedHeartbeatWindows) {
|
|
90951
91111
|
if (now - timestamp > this.HEARTBEAT_DEDUPE_TTL_MS) {
|
|
@@ -91319,19 +91479,163 @@ var init_timeback_service = __esm(async () => {
|
|
|
91319
91479
|
}
|
|
91320
91480
|
async getUserEnrollments(userId, gameId) {
|
|
91321
91481
|
return this.withClientTelemetry(async () => {
|
|
91322
|
-
const
|
|
91323
|
-
|
|
91324
|
-
|
|
91325
|
-
|
|
91326
|
-
|
|
91327
|
-
|
|
91482
|
+
const enrollments = await this.fetchEnrollments(await this.requireTimebackId(userId));
|
|
91483
|
+
return gameId ? filterEnrollmentsByGame(enrollments, gameId) : enrollments;
|
|
91484
|
+
});
|
|
91485
|
+
}
|
|
91486
|
+
async requireTimebackId(userId) {
|
|
91487
|
+
const userData = await this.deps.db.query.users.findFirst({
|
|
91488
|
+
where: eq(users.id, userId),
|
|
91489
|
+
columns: { id: true, timebackId: true }
|
|
91490
|
+
});
|
|
91491
|
+
if (!userData) {
|
|
91492
|
+
throw new NotFoundError("User", userId);
|
|
91493
|
+
}
|
|
91494
|
+
if (!userData.timebackId) {
|
|
91495
|
+
throw new NotFoundError("Timeback account not found for user");
|
|
91496
|
+
}
|
|
91497
|
+
return userData.timebackId;
|
|
91498
|
+
}
|
|
91499
|
+
async getHoleFillingContext(userId, gameId) {
|
|
91500
|
+
return this.withClientTelemetry(async () => {
|
|
91501
|
+
setAttribute("app.timeback.operation", "get_hole_filling_context");
|
|
91502
|
+
const studentId = await this.requireTimebackId(userId);
|
|
91503
|
+
const enrollments = filterEnrollmentsByGame(await this.fetchEnrollments(studentId), gameId);
|
|
91504
|
+
const holeFilling = enrollments.filter((enrollment) => enrollment.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND);
|
|
91505
|
+
setAttribute("app.timeback.hole_filling_enrollment_count", holeFilling.length);
|
|
91506
|
+
if (holeFilling.length === 0) {
|
|
91507
|
+
return { contexts: [] };
|
|
91328
91508
|
}
|
|
91329
|
-
|
|
91330
|
-
|
|
91509
|
+
const contexts = await Promise.all(holeFilling.map((enrollment) => this.buildHoleFillingContext(studentId, enrollment)));
|
|
91510
|
+
return { contexts };
|
|
91511
|
+
});
|
|
91512
|
+
}
|
|
91513
|
+
async buildHoleFillingContext(studentId, enrollment) {
|
|
91514
|
+
const client = this.requireClient();
|
|
91515
|
+
const grade = enrollment.grade;
|
|
91516
|
+
const subject = enrollment.subject;
|
|
91517
|
+
const enrollmentContext = {
|
|
91518
|
+
courseId: enrollment.courseId,
|
|
91519
|
+
baseCourseId: enrollment.baseCourseId ?? "",
|
|
91520
|
+
...enrollment.id && { enrollmentId: enrollment.id },
|
|
91521
|
+
grade,
|
|
91522
|
+
subject,
|
|
91523
|
+
beginDate: enrollment.beginDate ?? null
|
|
91524
|
+
};
|
|
91525
|
+
const stampedAssignmentId = assignmentIdOf({ metadata: enrollment.enrollmentMetadata });
|
|
91526
|
+
let parent = null;
|
|
91527
|
+
if (stampedAssignmentId) {
|
|
91528
|
+
const where = {
|
|
91529
|
+
...buildAssignmentResultWhere({
|
|
91530
|
+
studentId,
|
|
91531
|
+
assignmentId: stampedAssignmentId,
|
|
91532
|
+
resultType: "assessment"
|
|
91533
|
+
}),
|
|
91534
|
+
scoreStatus: SCORE_STATUS2.fullyGraded
|
|
91535
|
+
};
|
|
91536
|
+
const page = await client.api.oneroster.assessmentResults.list({
|
|
91537
|
+
limit: 1,
|
|
91538
|
+
where,
|
|
91539
|
+
sort: "scoreDate",
|
|
91540
|
+
orderBy: "desc"
|
|
91541
|
+
});
|
|
91542
|
+
parent = page.data[0] ?? null;
|
|
91543
|
+
}
|
|
91544
|
+
const summary = parent ? toTriggeringResult(parent) : null;
|
|
91545
|
+
let trigger = "not_stamped";
|
|
91546
|
+
if (stampedAssignmentId) {
|
|
91547
|
+
trigger = summary ? "found" : "not_found";
|
|
91548
|
+
}
|
|
91549
|
+
setAttributes({
|
|
91550
|
+
"app.timeback.hole_filling_trigger": trigger,
|
|
91551
|
+
"app.timeback.hole_filling_assignment_id": stampedAssignmentId
|
|
91552
|
+
});
|
|
91553
|
+
if (!parent || !summary) {
|
|
91554
|
+
return {
|
|
91555
|
+
enrollment: enrollmentContext,
|
|
91556
|
+
triggeringResult: null,
|
|
91557
|
+
missedStandards: [],
|
|
91558
|
+
untaggedMissedQuestionNumbers: []
|
|
91559
|
+
};
|
|
91560
|
+
}
|
|
91561
|
+
const cacheKey = `${enrollment.courseId}:${studentId}:${summary.resultId}`;
|
|
91562
|
+
const cached = this.holeFillingContextCache.get(cacheKey);
|
|
91563
|
+
if (cached && Date.now() - cached.at < TimebackService.HOLE_FILLING_CONTEXT_TTL_MS) {
|
|
91564
|
+
return { ...cached.context, enrollment: enrollmentContext };
|
|
91565
|
+
}
|
|
91566
|
+
const { context, complete } = await this.resolveMissedStandards(studentId, parent, summary, enrollmentContext);
|
|
91567
|
+
setAttribute("app.timeback.hole_filling_standards_complete", complete);
|
|
91568
|
+
if (complete) {
|
|
91569
|
+
setBounded(this.holeFillingContextCache, cacheKey, { at: Date.now(), context }, TimebackService.HOLE_FILLING_CONTEXT_CACHE_SIZE);
|
|
91570
|
+
}
|
|
91571
|
+
return context;
|
|
91572
|
+
}
|
|
91573
|
+
async resolveMissedStandards(studentId, parent, summary, enrollmentContext) {
|
|
91574
|
+
const client = this.requireClient();
|
|
91575
|
+
let complete = true;
|
|
91576
|
+
let missed = missedQuestionsFromInlineResponses(parent);
|
|
91577
|
+
if (!missed) {
|
|
91578
|
+
const where = buildAssignmentResultWhere({
|
|
91579
|
+
studentId,
|
|
91580
|
+
assignmentId: summary.assignmentId,
|
|
91581
|
+
resultType: "question"
|
|
91582
|
+
});
|
|
91583
|
+
const questionResults = await client.api.oneroster.assessmentResults.listAll({
|
|
91584
|
+
limit: 200,
|
|
91585
|
+
where,
|
|
91586
|
+
sort: "scoreDate",
|
|
91587
|
+
orderBy: "desc"
|
|
91588
|
+
});
|
|
91589
|
+
missed = missedQuestionsFromQuestionResults(questionResults);
|
|
91590
|
+
}
|
|
91591
|
+
let qtiTestId = "";
|
|
91592
|
+
let qtiQuestions = [];
|
|
91593
|
+
const standardsByItemId = new Map;
|
|
91594
|
+
if (missed.length > 0 && !client.hasQti) {
|
|
91595
|
+
complete = false;
|
|
91596
|
+
missed = missed.map(({ questionNumber }) => ({ questionNumber }));
|
|
91597
|
+
} else if (missed.some((question) => !question.qtiItemId)) {
|
|
91598
|
+
const lineItem = await client.api.oneroster.assessmentLineItems.get(summary.lineItemId).catch(() => null);
|
|
91599
|
+
qtiTestId = lineItem ? await resolveQtiTestId(client, resolveGradeLevelTestName(parent, lineItem), lineItem) : "";
|
|
91600
|
+
complete &&= Boolean(qtiTestId);
|
|
91601
|
+
if (qtiTestId) {
|
|
91602
|
+
qtiQuestions = await client.qtiApi.assessmentTests.getQuestions(qtiTestId).then((response) => response.questions ?? []).catch(() => {
|
|
91603
|
+
complete = false;
|
|
91604
|
+
return [];
|
|
91605
|
+
});
|
|
91606
|
+
for (const question of qtiQuestions) {
|
|
91607
|
+
const id = question.question.identifier || question.reference.identifier;
|
|
91608
|
+
if (id) {
|
|
91609
|
+
standardsByItemId.set(id, standardsFromQtiItem(question.question));
|
|
91610
|
+
}
|
|
91611
|
+
}
|
|
91331
91612
|
}
|
|
91332
|
-
|
|
91333
|
-
|
|
91613
|
+
}
|
|
91614
|
+
const itemIdsToFetch = [
|
|
91615
|
+
...new Set(missed.flatMap((question) => question.qtiItemId && !standardsByItemId.has(question.qtiItemId) ? [question.qtiItemId] : []))
|
|
91616
|
+
];
|
|
91617
|
+
const items = await Promise.all(itemIdsToFetch.map((id) => client.qtiApi.assessmentItems.get(id).then((item) => [id, standardsFromQtiItem(item)]).catch(() => {
|
|
91618
|
+
complete = false;
|
|
91619
|
+
return [id, []];
|
|
91620
|
+
})));
|
|
91621
|
+
for (const [id, standards] of items) {
|
|
91622
|
+
standardsByItemId.set(id, standards);
|
|
91623
|
+
}
|
|
91624
|
+
const { missedStandards, untaggedQuestionNumbers } = groupMissedStandards(missed, standardsByItemId, qtiItemIdForQuestionNumber(qtiQuestions));
|
|
91625
|
+
setAttributes({
|
|
91626
|
+
"app.timeback.hole_filling_missed_question_count": missed.length,
|
|
91627
|
+
"app.timeback.hole_filling_missed_standard_count": missedStandards.length,
|
|
91628
|
+
"app.timeback.hole_filling_untagged_question_count": untaggedQuestionNumbers.length
|
|
91334
91629
|
});
|
|
91630
|
+
return {
|
|
91631
|
+
context: {
|
|
91632
|
+
enrollment: enrollmentContext,
|
|
91633
|
+
triggeringResult: { ...summary, ...qtiTestId && { qtiTestId } },
|
|
91634
|
+
missedStandards,
|
|
91635
|
+
untaggedMissedQuestionNumbers: untaggedQuestionNumbers
|
|
91636
|
+
},
|
|
91637
|
+
complete
|
|
91638
|
+
};
|
|
91335
91639
|
}
|
|
91336
91640
|
async getUserDataByTimebackId(timebackId) {
|
|
91337
91641
|
return this.withClientTelemetry(async () => {
|
|
@@ -93564,6 +93868,13 @@ var init_embed_util = __esm(() => {
|
|
|
93564
93868
|
});
|
|
93565
93869
|
|
|
93566
93870
|
// ../api-core/src/services/embed.service.ts
|
|
93871
|
+
async function scopedContextId(partnerId, contextId) {
|
|
93872
|
+
return sha256Hex(`${partnerId}:${contextId}`);
|
|
93873
|
+
}
|
|
93874
|
+
function launchTokenClaims(launch) {
|
|
93875
|
+
return launch.contextId ? { contextId: launch.contextId } : {};
|
|
93876
|
+
}
|
|
93877
|
+
|
|
93567
93878
|
class EmbedService {
|
|
93568
93879
|
deps;
|
|
93569
93880
|
constructor(deps) {
|
|
@@ -93607,6 +93918,7 @@ class EmbedService {
|
|
|
93607
93918
|
gameId: game.id,
|
|
93608
93919
|
parentOrigin: input.parentOrigin,
|
|
93609
93920
|
timebackCourseId: input.timebackCourseId ?? null,
|
|
93921
|
+
contextId: input.contextId ? await scopedContextId(partner.id, input.contextId) : null,
|
|
93610
93922
|
passHash: await sha256Hex(pass),
|
|
93611
93923
|
passExpiresAt,
|
|
93612
93924
|
expiresAt: new Date(now + EMBED_LAUNCH_TTL_MS)
|
|
@@ -93614,7 +93926,11 @@ class EmbedService {
|
|
|
93614
93926
|
if (!launch) {
|
|
93615
93927
|
throw new InternalError("Failed to create embed launch");
|
|
93616
93928
|
}
|
|
93617
|
-
setAttributes({
|
|
93929
|
+
setAttributes({
|
|
93930
|
+
"app.launch.id": launch.id,
|
|
93931
|
+
"app.user.id": user.id,
|
|
93932
|
+
"app.embed.has_context": input.contextId !== undefined
|
|
93933
|
+
});
|
|
93618
93934
|
return { launchId: launch.id, pass, passExpiresAt, gameSlug: game.slug };
|
|
93619
93935
|
}
|
|
93620
93936
|
async redeemLaunch(input, timeZone) {
|
|
@@ -93629,7 +93945,7 @@ class EmbedService {
|
|
|
93629
93945
|
const [game, profile, token] = await Promise.all([
|
|
93630
93946
|
this.deps.getGameForStudent(launch.gameId, user),
|
|
93631
93947
|
this.deps.getGameProfile(user, launch.gameId, timeZone),
|
|
93632
|
-
this.deps.mintGameToken(launch.gameId, user.id)
|
|
93948
|
+
this.deps.mintGameToken(launch.gameId, user.id, launchTokenClaims(launch))
|
|
93633
93949
|
]);
|
|
93634
93950
|
await this.deps.db.update(embedLaunches).set({ redeemedAt: new Date }).where(eq(embedLaunches.id, launch.id));
|
|
93635
93951
|
return {
|
|
@@ -93661,7 +93977,7 @@ class EmbedService {
|
|
|
93661
93977
|
if (outcome) {
|
|
93662
93978
|
throw new AccessDeniedError("This launch has ended");
|
|
93663
93979
|
}
|
|
93664
|
-
return this.deps.mintGameToken(launch.gameId, launch.userId);
|
|
93980
|
+
return this.deps.mintGameToken(launch.gameId, launch.userId, launchTokenClaims(launch));
|
|
93665
93981
|
}
|
|
93666
93982
|
async endLaunch(launchId) {
|
|
93667
93983
|
await this.deps.db.update(embedLaunches).set({ endedAt: sql`coalesce(${embedLaunches.endedAt}, now())`, passHash: null }).where(eq(embedLaunches.id, launchId));
|
|
@@ -94763,10 +95079,10 @@ function createSandboxAuthProvider() {
|
|
|
94763
95079
|
}
|
|
94764
95080
|
return deleted;
|
|
94765
95081
|
},
|
|
94766
|
-
async mintGameToken(gameId, userId) {
|
|
95082
|
+
async mintGameToken(gameId, userId, claims) {
|
|
94767
95083
|
const header = btoa(JSON.stringify({ alg: "none", typ: "sandbox" }));
|
|
94768
95084
|
const exp = Math.floor(Date.now() / 1000) + 15 * 60;
|
|
94769
|
-
const payload = btoa(JSON.stringify({ sub: gameId, uid: userId, exp }));
|
|
95085
|
+
const payload = btoa(JSON.stringify({ ...claims, sub: gameId, uid: userId, exp }));
|
|
94770
95086
|
const token = `${header}.${payload}.sandbox`;
|
|
94771
95087
|
return { token, exp: exp * 1000 };
|
|
94772
95088
|
},
|
|
@@ -94891,33 +95207,33 @@ var init_auth_provider = __esm(async () => {
|
|
|
94891
95207
|
function createSandboxCacheProvider() {
|
|
94892
95208
|
return {
|
|
94893
95209
|
async get(key) {
|
|
94894
|
-
const entry =
|
|
95210
|
+
const entry = cache2.get(key);
|
|
94895
95211
|
if (!entry) {
|
|
94896
95212
|
return null;
|
|
94897
95213
|
}
|
|
94898
95214
|
if (entry.expiresAt < Date.now()) {
|
|
94899
|
-
|
|
95215
|
+
cache2.delete(key);
|
|
94900
95216
|
return null;
|
|
94901
95217
|
}
|
|
94902
95218
|
return entry.value;
|
|
94903
95219
|
},
|
|
94904
95220
|
async set(key, value, ttlSeconds = 300) {
|
|
94905
|
-
|
|
95221
|
+
cache2.set(key, {
|
|
94906
95222
|
value,
|
|
94907
95223
|
expiresAt: Date.now() + ttlSeconds * 1000
|
|
94908
95224
|
});
|
|
94909
95225
|
},
|
|
94910
95226
|
async delete(key) {
|
|
94911
|
-
|
|
95227
|
+
cache2.delete(key);
|
|
94912
95228
|
}
|
|
94913
95229
|
};
|
|
94914
95230
|
}
|
|
94915
95231
|
function clearSandboxCache() {
|
|
94916
|
-
|
|
95232
|
+
cache2.clear();
|
|
94917
95233
|
}
|
|
94918
|
-
var
|
|
95234
|
+
var cache2;
|
|
94919
95235
|
var init_cache_provider = __esm(() => {
|
|
94920
|
-
|
|
95236
|
+
cache2 = new Map;
|
|
94921
95237
|
});
|
|
94922
95238
|
|
|
94923
95239
|
// ../utils/src/keys.ts
|
|
@@ -151145,7 +151461,7 @@ function parseQtiLibraryParams(searchParams) {
|
|
|
151145
151461
|
limit
|
|
151146
151462
|
};
|
|
151147
151463
|
}
|
|
151148
|
-
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;
|
|
151464
|
+
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;
|
|
151149
151465
|
var init_timeback_controller = __esm(() => {
|
|
151150
151466
|
init_esm();
|
|
151151
151467
|
init_src();
|
|
@@ -151171,6 +151487,12 @@ var init_timeback_controller = __esm(() => {
|
|
|
151171
151487
|
});
|
|
151172
151488
|
getUser = requireNonAnonymous(async (ctx) => ctx.services.timeback.getUserData(ctx.user.id, ctx.gameId));
|
|
151173
151489
|
getUserEnrollments = requireNonAnonymous(async (ctx) => ctx.services.timeback.getUserEnrollments(ctx.user.id, ctx.gameId));
|
|
151490
|
+
getUserHoleFillingContext = requireNonAnonymous(async (ctx) => {
|
|
151491
|
+
if (!ctx.gameId) {
|
|
151492
|
+
throw ApiError.forbidden("Hole-filling context requires a game token");
|
|
151493
|
+
}
|
|
151494
|
+
return ctx.services.timeback.getHoleFillingContext(ctx.user.id, ctx.gameId);
|
|
151495
|
+
});
|
|
151174
151496
|
getUserById = requireNonAnonymous(async (ctx) => {
|
|
151175
151497
|
const timebackId = ctx.params.timebackId;
|
|
151176
151498
|
if (!timebackId) {
|
|
@@ -152024,6 +152346,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
152024
152346
|
populateStudent,
|
|
152025
152347
|
getUser,
|
|
152026
152348
|
getUserEnrollments,
|
|
152349
|
+
getUserHoleFillingContext,
|
|
152027
152350
|
getUserById,
|
|
152028
152351
|
setupIntegration,
|
|
152029
152352
|
getIntegrations,
|
|
@@ -153086,6 +153409,12 @@ var init_timeback8 = __esm(async () => {
|
|
|
153086
153409
|
}
|
|
153087
153410
|
return handle2(timeback2.getUserEnrollments)(c);
|
|
153088
153411
|
});
|
|
153412
|
+
timebackRouter.get("/user/hole-filling-context", async (c) => {
|
|
153413
|
+
if (shouldMockTimeback()) {
|
|
153414
|
+
return c.json({ contexts: [] });
|
|
153415
|
+
}
|
|
153416
|
+
return handle2(timeback2.getUserHoleFillingContext)(c);
|
|
153417
|
+
});
|
|
153089
153418
|
timebackRouter.get("/user/:timebackId", async (c) => {
|
|
153090
153419
|
const user = c.get("user");
|
|
153091
153420
|
if (!user) {
|