@playcademy/sandbox 0.10.2-beta.1 → 0.10.2-beta.3
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 +379 -99
- package/dist/constants.js +11 -1
- package/dist/server.js +379 -99
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -282,7 +282,17 @@ var init_platform = __esm(() => {
|
|
|
282
282
|
});
|
|
283
283
|
|
|
284
284
|
// ../constants/src/skills.ts
|
|
285
|
-
var
|
|
285
|
+
var SKILL_STATUS_ROUTE = "/__playcademy/skills/status", SKILL_STATUS_TOKEN_AUDIENCE = "playcademy-game-skill-status", SKILL_STATUS_MAX_SKILLS = 50, SKILL_STATUSES, SKILL_MASTERY_SOURCES;
|
|
286
|
+
var init_skills = __esm(() => {
|
|
287
|
+
SKILL_STATUSES = [
|
|
288
|
+
"locked",
|
|
289
|
+
"unlocked",
|
|
290
|
+
"in_progress",
|
|
291
|
+
"mastered",
|
|
292
|
+
"unknown_skill"
|
|
293
|
+
];
|
|
294
|
+
SKILL_MASTERY_SOURCES = ["assessment", "placement", "test_out", "manual"];
|
|
295
|
+
});
|
|
286
296
|
|
|
287
297
|
// ../constants/src/system.ts
|
|
288
298
|
var PLATFORM_TIMEZONE = "America/New_York";
|
|
@@ -1132,7 +1142,7 @@ var package_default;
|
|
|
1132
1142
|
var init_package = __esm(() => {
|
|
1133
1143
|
package_default = {
|
|
1134
1144
|
name: "@playcademy/sandbox",
|
|
1135
|
-
version: "0.10.2-beta.
|
|
1145
|
+
version: "0.10.2-beta.3",
|
|
1136
1146
|
description: "Local development server for Playcademy game development",
|
|
1137
1147
|
type: "module",
|
|
1138
1148
|
exports: {
|
|
@@ -30009,6 +30019,23 @@ function generateDeploymentHash(code) {
|
|
|
30009
30019
|
function computeDeployPayloadFingerprint(payload) {
|
|
30010
30020
|
return sha256Hex(canonicalJson2(payload));
|
|
30011
30021
|
}
|
|
30022
|
+
function toActiveDeploymentRow(record) {
|
|
30023
|
+
return {
|
|
30024
|
+
gameId: record.gameId,
|
|
30025
|
+
deploymentId: record.deploymentId,
|
|
30026
|
+
provider: "cloudflare",
|
|
30027
|
+
target: record.target,
|
|
30028
|
+
url: record.url,
|
|
30029
|
+
codeHash: record.codeHash,
|
|
30030
|
+
schemaHash: record.schemaHash ?? null,
|
|
30031
|
+
schemaFingerprint: record.schemaFingerprint ?? null,
|
|
30032
|
+
timeTravelBookmark: record.timeTravelBookmark ?? null,
|
|
30033
|
+
bookmarkCapturedAt: record.bookmarkCapturedAt ?? null,
|
|
30034
|
+
resources: record.resources,
|
|
30035
|
+
isActive: true,
|
|
30036
|
+
deployJobId: record.deployJobId ?? null
|
|
30037
|
+
};
|
|
30038
|
+
}
|
|
30012
30039
|
var GAME_WORKER_KEY_PREFIX = "game-worker-", DASHBOARD_WORKER_KEY_PREFIX = "dash-worker-";
|
|
30013
30040
|
var init_deployment_util = __esm(() => {
|
|
30014
30041
|
init_drizzle_orm();
|
|
@@ -37032,24 +37059,9 @@ class DeployService {
|
|
|
37032
37059
|
}
|
|
37033
37060
|
}
|
|
37034
37061
|
async saveDeployment(record) {
|
|
37035
|
-
|
|
37036
|
-
await db2.transaction(async (tx) => {
|
|
37062
|
+
await this.deps.db.transaction(async (tx) => {
|
|
37037
37063
|
await tx.update(gameDeployments).set({ isActive: false }).where(and(eq(gameDeployments.gameId, record.gameId), eq(gameDeployments.target, record.target)));
|
|
37038
|
-
await tx.insert(gameDeployments).values(
|
|
37039
|
-
gameId: record.gameId,
|
|
37040
|
-
deploymentId: record.deploymentId,
|
|
37041
|
-
provider: "cloudflare",
|
|
37042
|
-
target: record.target,
|
|
37043
|
-
url: record.url,
|
|
37044
|
-
codeHash: record.codeHash,
|
|
37045
|
-
schemaHash: record.schemaHash ?? null,
|
|
37046
|
-
schemaFingerprint: record.schemaFingerprint ?? null,
|
|
37047
|
-
timeTravelBookmark: record.timeTravelBookmark ?? null,
|
|
37048
|
-
bookmarkCapturedAt: record.bookmarkCapturedAt ?? null,
|
|
37049
|
-
resources: record.resources,
|
|
37050
|
-
isActive: true,
|
|
37051
|
-
deployJobId: record.deployJobId ?? null
|
|
37052
|
-
});
|
|
37064
|
+
await tx.insert(gameDeployments).values(toActiveDeploymentRow(record));
|
|
37053
37065
|
});
|
|
37054
37066
|
}
|
|
37055
37067
|
async notifyDeploymentFailure(failure) {
|
|
@@ -46881,6 +46893,39 @@ function publishableRepositoryUrl(value) {
|
|
|
46881
46893
|
return value;
|
|
46882
46894
|
}
|
|
46883
46895
|
|
|
46896
|
+
// ../api-core/src/utils/releases.util.ts
|
|
46897
|
+
function toRelease(row) {
|
|
46898
|
+
return {
|
|
46899
|
+
slug: row.slug,
|
|
46900
|
+
url: row.url,
|
|
46901
|
+
source: normalizeDeploySource(row.source),
|
|
46902
|
+
deployedAt: row.deployedAt.toISOString()
|
|
46903
|
+
};
|
|
46904
|
+
}
|
|
46905
|
+
function toGameReleaseResponse(rows, history) {
|
|
46906
|
+
const releases = rows.map(toRelease);
|
|
46907
|
+
const current = releases[0] ?? null;
|
|
46908
|
+
return history === undefined ? { current } : { current, history: releases };
|
|
46909
|
+
}
|
|
46910
|
+
function normalizeDeploySource(value) {
|
|
46911
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
46912
|
+
return null;
|
|
46913
|
+
}
|
|
46914
|
+
const raw = value;
|
|
46915
|
+
const repository = nullableString(raw.repository);
|
|
46916
|
+
return {
|
|
46917
|
+
repository: repository === null ? null : publishableRepositoryUrl(repository),
|
|
46918
|
+
commit: nullableString(raw.commit),
|
|
46919
|
+
branch: nullableString(raw.branch),
|
|
46920
|
+
dirty: raw.dirty === true,
|
|
46921
|
+
externalBuild: raw.externalBuild === true
|
|
46922
|
+
};
|
|
46923
|
+
}
|
|
46924
|
+
function nullableString(value) {
|
|
46925
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
46926
|
+
}
|
|
46927
|
+
var init_releases_util = () => {};
|
|
46928
|
+
|
|
46884
46929
|
// ../api-core/src/services/releases.service.ts
|
|
46885
46930
|
class ReleasesService {
|
|
46886
46931
|
deps;
|
|
@@ -46905,9 +46950,7 @@ class ReleasesService {
|
|
|
46905
46950
|
"app.releases.history_requested": history ?? 0,
|
|
46906
46951
|
"app.releases.history_rows": rows.length
|
|
46907
46952
|
});
|
|
46908
|
-
|
|
46909
|
-
const current = releases[0] ?? null;
|
|
46910
|
-
return history === undefined ? { current } : { current, history: releases };
|
|
46953
|
+
return toGameReleaseResponse(rows, history);
|
|
46911
46954
|
}
|
|
46912
46955
|
}
|
|
46913
46956
|
function releaseColumns() {
|
|
@@ -46924,36 +46967,12 @@ function listedGame() {
|
|
|
46924
46967
|
function activeWorkerDeployment() {
|
|
46925
46968
|
return and(eq(gameDeployments.isActive, true), eq(gameDeployments.target, "game"));
|
|
46926
46969
|
}
|
|
46927
|
-
function toRelease(row) {
|
|
46928
|
-
return {
|
|
46929
|
-
slug: row.slug,
|
|
46930
|
-
url: row.url,
|
|
46931
|
-
source: normalizeSource(row.source),
|
|
46932
|
-
deployedAt: row.deployedAt.toISOString()
|
|
46933
|
-
};
|
|
46934
|
-
}
|
|
46935
|
-
function normalizeSource(value) {
|
|
46936
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
46937
|
-
return null;
|
|
46938
|
-
}
|
|
46939
|
-
const raw = value;
|
|
46940
|
-
const repository = nullableString(raw.repository);
|
|
46941
|
-
return {
|
|
46942
|
-
repository: repository === null ? null : publishableRepositoryUrl(repository),
|
|
46943
|
-
commit: nullableString(raw.commit),
|
|
46944
|
-
branch: nullableString(raw.branch),
|
|
46945
|
-
dirty: raw.dirty === true,
|
|
46946
|
-
externalBuild: raw.externalBuild === true
|
|
46947
|
-
};
|
|
46948
|
-
}
|
|
46949
|
-
function nullableString(value) {
|
|
46950
|
-
return typeof value === "string" && value.length > 0 ? value : null;
|
|
46951
|
-
}
|
|
46952
46970
|
var init_releases_service = __esm(() => {
|
|
46953
46971
|
init_drizzle_orm();
|
|
46954
46972
|
init_tables_index();
|
|
46955
46973
|
init_spans();
|
|
46956
46974
|
init_errors2();
|
|
46975
|
+
init_releases_util();
|
|
46957
46976
|
});
|
|
46958
46977
|
|
|
46959
46978
|
// ../api-core/src/services/secrets.service.ts
|
|
@@ -47409,6 +47428,226 @@ var init_seed_service = __esm(() => {
|
|
|
47409
47428
|
init_deployment_util();
|
|
47410
47429
|
});
|
|
47411
47430
|
|
|
47431
|
+
// ../api-core/src/utils/game-worker-call.util.ts
|
|
47432
|
+
function resolveGameWorkerBaseUrl(localGameUrls, slug, deployedUrl) {
|
|
47433
|
+
return (slug ? localGameUrls[slug] : undefined) ?? deployedUrl;
|
|
47434
|
+
}
|
|
47435
|
+
async function callGameWorker(url, token, options) {
|
|
47436
|
+
const controller = new AbortController;
|
|
47437
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
47438
|
+
try {
|
|
47439
|
+
const response = await fetch(url, {
|
|
47440
|
+
method: "GET",
|
|
47441
|
+
headers: {
|
|
47442
|
+
Accept: "application/json",
|
|
47443
|
+
Authorization: `Bearer ${token}`
|
|
47444
|
+
},
|
|
47445
|
+
signal: controller.signal
|
|
47446
|
+
});
|
|
47447
|
+
const body2 = await response.text();
|
|
47448
|
+
return {
|
|
47449
|
+
ok: true,
|
|
47450
|
+
response: new Response(NULL_BODY_STATUSES.has(response.status) ? null : body2, {
|
|
47451
|
+
status: response.status,
|
|
47452
|
+
headers: response.headers
|
|
47453
|
+
})
|
|
47454
|
+
};
|
|
47455
|
+
} catch (error) {
|
|
47456
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
47457
|
+
return { ok: false, reason: "timeout", details: options.timeoutMessage };
|
|
47458
|
+
}
|
|
47459
|
+
return { ok: false, reason: "fetch_failed", details: errorMessage(error) };
|
|
47460
|
+
} finally {
|
|
47461
|
+
clearTimeout(timeout);
|
|
47462
|
+
}
|
|
47463
|
+
}
|
|
47464
|
+
var NULL_BODY_STATUSES;
|
|
47465
|
+
var init_game_worker_call_util = __esm(() => {
|
|
47466
|
+
NULL_BODY_STATUSES = new Set([101, 204, 205, 304]);
|
|
47467
|
+
});
|
|
47468
|
+
|
|
47469
|
+
// ../utils/src/skills.ts
|
|
47470
|
+
function normalizeSkillId(skill) {
|
|
47471
|
+
return skill.trim().toLowerCase();
|
|
47472
|
+
}
|
|
47473
|
+
function normalizeSkillIds(skills2, max) {
|
|
47474
|
+
const normalized = [...new Set(skills2.map(normalizeSkillId).filter((skill) => skill.length > 0))];
|
|
47475
|
+
if (normalized.length === 0) {
|
|
47476
|
+
return { ok: false, error: "At least one skill is required" };
|
|
47477
|
+
}
|
|
47478
|
+
if (normalized.length > max) {
|
|
47479
|
+
return { ok: false, error: `At most ${max} skills can be requested at once` };
|
|
47480
|
+
}
|
|
47481
|
+
return { ok: true, skills: normalized };
|
|
47482
|
+
}
|
|
47483
|
+
|
|
47484
|
+
// ../edge-play/src/game/lib/skills.ts
|
|
47485
|
+
function isValidIsoDate(value) {
|
|
47486
|
+
if (typeof value !== "string" || !ISO_DATE_PATTERN.test(value)) {
|
|
47487
|
+
return false;
|
|
47488
|
+
}
|
|
47489
|
+
const [year, month, day] = value.slice(0, 10).split("-").map(Number);
|
|
47490
|
+
const date3 = new Date(Date.UTC(year, month - 1, day));
|
|
47491
|
+
return !isNaN(Date.parse(value)) && date3.getUTCFullYear() === year && date3.getUTCMonth() === month - 1 && date3.getUTCDate() === day;
|
|
47492
|
+
}
|
|
47493
|
+
function normalizeResult(result) {
|
|
47494
|
+
if (!isRecord(result)) {
|
|
47495
|
+
return null;
|
|
47496
|
+
}
|
|
47497
|
+
if (typeof result.skill !== "string" || !STATUSES.has(result.status)) {
|
|
47498
|
+
return null;
|
|
47499
|
+
}
|
|
47500
|
+
const status = result.status;
|
|
47501
|
+
if (result.source !== undefined) {
|
|
47502
|
+
if (status !== "mastered" || !MASTERY_SOURCES.has(result.source)) {
|
|
47503
|
+
return null;
|
|
47504
|
+
}
|
|
47505
|
+
}
|
|
47506
|
+
if (result.asOf !== undefined && !isValidIsoDate(result.asOf)) {
|
|
47507
|
+
return null;
|
|
47508
|
+
}
|
|
47509
|
+
return {
|
|
47510
|
+
skill: normalizeSkillId(result.skill),
|
|
47511
|
+
status,
|
|
47512
|
+
...result.source === undefined ? {} : { source: result.source },
|
|
47513
|
+
...result.asOf === undefined ? {} : { asOf: result.asOf }
|
|
47514
|
+
};
|
|
47515
|
+
}
|
|
47516
|
+
function validateSkillStatusResponse(value, studentId, askedSkills) {
|
|
47517
|
+
if (!isRecord(value)) {
|
|
47518
|
+
return null;
|
|
47519
|
+
}
|
|
47520
|
+
const response = value;
|
|
47521
|
+
if (response.studentId !== studentId || !Array.isArray(response.results)) {
|
|
47522
|
+
return null;
|
|
47523
|
+
}
|
|
47524
|
+
const asked = new Set(askedSkills);
|
|
47525
|
+
const results = new Map;
|
|
47526
|
+
for (const raw of response.results) {
|
|
47527
|
+
const result = normalizeResult(raw);
|
|
47528
|
+
if (!result || !asked.has(result.skill) || results.has(result.skill)) {
|
|
47529
|
+
return null;
|
|
47530
|
+
}
|
|
47531
|
+
results.set(result.skill, result);
|
|
47532
|
+
}
|
|
47533
|
+
return results.size === asked.size ? results : null;
|
|
47534
|
+
}
|
|
47535
|
+
var STATUSES, MASTERY_SOURCES, ISO_DATE_PATTERN;
|
|
47536
|
+
var init_skills2 = __esm(() => {
|
|
47537
|
+
init_src();
|
|
47538
|
+
STATUSES = new Set(SKILL_STATUSES);
|
|
47539
|
+
MASTERY_SOURCES = new Set(SKILL_MASTERY_SOURCES);
|
|
47540
|
+
ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:\d{2}))?$/;
|
|
47541
|
+
});
|
|
47542
|
+
|
|
47543
|
+
// ../api-core/src/utils/skill-status.util.ts
|
|
47544
|
+
function normalizeSkillStatusRequest(skills2) {
|
|
47545
|
+
const normalized = normalizeSkillIds(skills2, SKILL_STATUS_MAX_SKILLS);
|
|
47546
|
+
if (!normalized.ok) {
|
|
47547
|
+
throw new ValidationError(normalized.error);
|
|
47548
|
+
}
|
|
47549
|
+
return normalized.skills;
|
|
47550
|
+
}
|
|
47551
|
+
function buildSkillStatusUrl(baseUrl, skills2) {
|
|
47552
|
+
const url = new URL(SKILL_STATUS_ROUTE, baseUrl);
|
|
47553
|
+
for (const skill of skills2) {
|
|
47554
|
+
url.searchParams.append("skill", skill);
|
|
47555
|
+
}
|
|
47556
|
+
return url;
|
|
47557
|
+
}
|
|
47558
|
+
function pickSkillStatusGames(rows, requestingSlug, answeringSlug) {
|
|
47559
|
+
const requestingGame = rows.find((row) => row.slug === requestingSlug);
|
|
47560
|
+
const answeringGame = rows.find((row) => row.slug === answeringSlug);
|
|
47561
|
+
if (!requestingGame) {
|
|
47562
|
+
throw new NotFoundError("Game", requestingSlug);
|
|
47563
|
+
}
|
|
47564
|
+
if (!answeringGame) {
|
|
47565
|
+
throw new NotFoundError("Game", answeringSlug);
|
|
47566
|
+
}
|
|
47567
|
+
return { requestingGame, answeringGame };
|
|
47568
|
+
}
|
|
47569
|
+
async function classifySkillStatusResponse(response, studentId, skills2) {
|
|
47570
|
+
if (response.status === 404) {
|
|
47571
|
+
return { supported: false, reason: "not_implemented" };
|
|
47572
|
+
}
|
|
47573
|
+
if (!response.ok) {
|
|
47574
|
+
return { supported: false, reason: "fetch_failed" };
|
|
47575
|
+
}
|
|
47576
|
+
let json3;
|
|
47577
|
+
try {
|
|
47578
|
+
json3 = JSON.parse(await response.text());
|
|
47579
|
+
} catch {
|
|
47580
|
+
return { supported: false, reason: "not_implemented" };
|
|
47581
|
+
}
|
|
47582
|
+
const answered = validateSkillStatusResponse(json3, studentId, skills2);
|
|
47583
|
+
if (!answered) {
|
|
47584
|
+
return { supported: false, reason: "invalid_response" };
|
|
47585
|
+
}
|
|
47586
|
+
return { supported: true, results: skills2.map((skill) => answered.get(skill)) };
|
|
47587
|
+
}
|
|
47588
|
+
var init_skill_status_util = __esm(() => {
|
|
47589
|
+
init_src();
|
|
47590
|
+
init_skills2();
|
|
47591
|
+
init_errors2();
|
|
47592
|
+
});
|
|
47593
|
+
|
|
47594
|
+
// ../api-core/src/services/skill-status.service.ts
|
|
47595
|
+
class SkillStatusService {
|
|
47596
|
+
deps;
|
|
47597
|
+
constructor(deps) {
|
|
47598
|
+
this.deps = deps;
|
|
47599
|
+
}
|
|
47600
|
+
async getStatus(input) {
|
|
47601
|
+
const skills2 = normalizeSkillStatusRequest(input.skills);
|
|
47602
|
+
setAttributes({
|
|
47603
|
+
"app.skill_status.requesting_game": input.requestingGameSlug,
|
|
47604
|
+
"app.skill_status.answering_game": input.game,
|
|
47605
|
+
"app.skill_status.skill_count": skills2.length
|
|
47606
|
+
});
|
|
47607
|
+
const rows = await this.deps.db.select({ id: games.id, slug: games.slug }).from(games).where(inArray(games.slug, [input.requestingGameSlug, input.game]));
|
|
47608
|
+
const { requestingGame, answeringGame } = pickSkillStatusGames(rows, input.requestingGameSlug, input.game);
|
|
47609
|
+
const deployment = await this.deps.db.query.gameDeployments.findFirst({
|
|
47610
|
+
where: activeDeploymentWhere(answeringGame.id),
|
|
47611
|
+
columns: { url: true }
|
|
47612
|
+
});
|
|
47613
|
+
const baseUrl = resolveGameWorkerBaseUrl(this.deps.localGameUrls, answeringGame.slug, deployment?.url);
|
|
47614
|
+
if (!baseUrl) {
|
|
47615
|
+
return recordResult({ supported: false, reason: "no_active_deployment" });
|
|
47616
|
+
}
|
|
47617
|
+
const token = await this.deps.mintSkillStatusToken({
|
|
47618
|
+
gameId: answeringGame.id,
|
|
47619
|
+
studentId: input.studentId,
|
|
47620
|
+
requestingGame
|
|
47621
|
+
});
|
|
47622
|
+
const call = await callGameWorker(buildSkillStatusUrl(baseUrl, skills2), token, {
|
|
47623
|
+
timeoutMs: SKILL_STATUS_TIMEOUT_MS,
|
|
47624
|
+
timeoutMessage: "Skill status request timed out"
|
|
47625
|
+
});
|
|
47626
|
+
if (!call.ok) {
|
|
47627
|
+
setAttributes({ "app.skill_status.failure_details": call.details });
|
|
47628
|
+
return recordResult({ supported: false, reason: call.reason });
|
|
47629
|
+
}
|
|
47630
|
+
setAttributes({ "app.skill_status.http_status": call.response.status });
|
|
47631
|
+
return recordResult(await classifySkillStatusResponse(call.response, input.studentId, skills2));
|
|
47632
|
+
}
|
|
47633
|
+
}
|
|
47634
|
+
function recordResult(result) {
|
|
47635
|
+
setAttributes(result.supported ? {
|
|
47636
|
+
"app.skill_status.supported": true,
|
|
47637
|
+
"app.skill_status.unknown_skill_count": result.results.filter((entry) => entry.status === "unknown_skill").length
|
|
47638
|
+
} : { "app.skill_status.supported": false, "app.skill_status.reason": result.reason });
|
|
47639
|
+
return result;
|
|
47640
|
+
}
|
|
47641
|
+
var SKILL_STATUS_TIMEOUT_MS = 3000;
|
|
47642
|
+
var init_skill_status_service = __esm(() => {
|
|
47643
|
+
init_drizzle_orm();
|
|
47644
|
+
init_helpers_index();
|
|
47645
|
+
init_tables_index();
|
|
47646
|
+
init_spans();
|
|
47647
|
+
init_game_worker_call_util();
|
|
47648
|
+
init_skill_status_util();
|
|
47649
|
+
});
|
|
47650
|
+
|
|
47412
47651
|
// ../../node_modules/.bun/drizzle-zod@0.7.1+3ad0422b77d1a05b/node_modules/drizzle-zod/index.mjs
|
|
47413
47652
|
function isColumnType(column2, columnTypes) {
|
|
47414
47653
|
return columnTypes.includes(column2.columnType);
|
|
@@ -98258,12 +98497,6 @@ class TimebackAdminService {
|
|
|
98258
98497
|
grade: integration.grade
|
|
98259
98498
|
};
|
|
98260
98499
|
}
|
|
98261
|
-
resolveGameMetricsBaseUrl(slug2, deployedUrl) {
|
|
98262
|
-
if (!slug2) {
|
|
98263
|
-
return deployedUrl;
|
|
98264
|
-
}
|
|
98265
|
-
return this.deps.config.localGameUrls[slug2] ?? deployedUrl;
|
|
98266
|
-
}
|
|
98267
98500
|
static resolveGameMetricsUrl(baseUrl, runIds) {
|
|
98268
98501
|
try {
|
|
98269
98502
|
const url4 = new URL("/__playcademy/metrics", baseUrl);
|
|
@@ -98297,9 +98530,6 @@ class TimebackAdminService {
|
|
|
98297
98530
|
}
|
|
98298
98531
|
return chunks;
|
|
98299
98532
|
}
|
|
98300
|
-
static isAbortError(error88) {
|
|
98301
|
-
return error88 instanceof Error && error88.name === "AbortError";
|
|
98302
|
-
}
|
|
98303
98533
|
static roundXpToTenths(value) {
|
|
98304
98534
|
const rounded = Math.round(value * TimebackAdminService.XP_PRECISION_FACTOR) / TimebackAdminService.XP_PRECISION_FACTOR;
|
|
98305
98535
|
return Object.is(rounded, -0) ? 0 : rounded;
|
|
@@ -98937,7 +99167,7 @@ class TimebackAdminService {
|
|
|
98937
99167
|
if (enrollmentsByCourseId.size === 0) {
|
|
98938
99168
|
throw new NotFoundError("Student enrollment", `${timebackId}:${gameId}`);
|
|
98939
99169
|
}
|
|
98940
|
-
const metricsBaseUrl = this.
|
|
99170
|
+
const metricsBaseUrl = resolveGameWorkerBaseUrl(this.deps.config.localGameUrls, game2?.slug, deployment?.url);
|
|
98941
99171
|
if (!metricsBaseUrl) {
|
|
98942
99172
|
setAttributes({
|
|
98943
99173
|
"app.timeback.metrics.supported": false,
|
|
@@ -98960,40 +99190,18 @@ class TimebackAdminService {
|
|
|
98960
99190
|
};
|
|
98961
99191
|
}
|
|
98962
99192
|
const token = await this.deps.mintPlatformServiceToken(gameId, targetUser.id);
|
|
98963
|
-
const
|
|
98964
|
-
|
|
98965
|
-
|
|
98966
|
-
|
|
98967
|
-
|
|
98968
|
-
method: "GET",
|
|
98969
|
-
headers: {
|
|
98970
|
-
Accept: "application/json",
|
|
98971
|
-
Authorization: `Bearer ${token}`
|
|
98972
|
-
},
|
|
98973
|
-
signal: controller.signal
|
|
98974
|
-
});
|
|
98975
|
-
} catch (error88) {
|
|
98976
|
-
const timedOut = TimebackAdminService.isAbortError(error88);
|
|
98977
|
-
let details;
|
|
98978
|
-
if (timedOut) {
|
|
98979
|
-
details = "Game metrics request timed out";
|
|
98980
|
-
} else if (error88 instanceof Error) {
|
|
98981
|
-
details = error88.message;
|
|
98982
|
-
} else {
|
|
98983
|
-
details = String(error88);
|
|
98984
|
-
}
|
|
99193
|
+
const call = await callGameWorker(metricsUrl, token, {
|
|
99194
|
+
timeoutMs: options?.timeoutMs ?? TimebackAdminService.GAME_METRICS_FETCH_TIMEOUT_MS,
|
|
99195
|
+
timeoutMessage: "Game metrics request timed out"
|
|
99196
|
+
});
|
|
99197
|
+
if (!call.ok) {
|
|
98985
99198
|
setAttributes({
|
|
98986
99199
|
"app.timeback.metrics.supported": false,
|
|
98987
|
-
"app.timeback.metrics.reason":
|
|
99200
|
+
"app.timeback.metrics.reason": call.reason
|
|
98988
99201
|
});
|
|
98989
|
-
return {
|
|
98990
|
-
supported: false,
|
|
98991
|
-
reason: timedOut ? "timeout" : "fetch_failed",
|
|
98992
|
-
details
|
|
98993
|
-
};
|
|
98994
|
-
} finally {
|
|
98995
|
-
clearTimeout(timeout);
|
|
99202
|
+
return { supported: false, reason: call.reason, details: call.details };
|
|
98996
99203
|
}
|
|
99204
|
+
const { response } = call;
|
|
98997
99205
|
const result = await classifyGameMetricsResponse(response, { gameId, timebackId });
|
|
98998
99206
|
setAttributes({
|
|
98999
99207
|
"app.timeback.metrics.supported": result.supported,
|
|
@@ -99991,6 +100199,7 @@ var init_timeback_admin_service = __esm(async () => {
|
|
|
99991
100199
|
init_src9();
|
|
99992
100200
|
init_timeback3();
|
|
99993
100201
|
init_errors2();
|
|
100202
|
+
init_game_worker_call_util();
|
|
99994
100203
|
init_timeback_admin_metrics_util();
|
|
99995
100204
|
init_timeback_admin_util();
|
|
99996
100205
|
init_timeback_discrepancy_queue_util();
|
|
@@ -109902,6 +110111,7 @@ function createPlatformServices(deps) {
|
|
|
109902
110111
|
timebackClient,
|
|
109903
110112
|
alerts,
|
|
109904
110113
|
mintPlatformServiceToken,
|
|
110114
|
+
mintSkillStatusToken,
|
|
109905
110115
|
mintAssessmentPreparationReceipt,
|
|
109906
110116
|
verifyAssessmentPreparationReceipt,
|
|
109907
110117
|
mintAssessmentToken,
|
|
@@ -109985,6 +110195,11 @@ function createPlatformServices(deps) {
|
|
|
109985
110195
|
mintAssessmentToken,
|
|
109986
110196
|
verifyAssessmentToken
|
|
109987
110197
|
});
|
|
110198
|
+
const skillStatus = new SkillStatusService({
|
|
110199
|
+
db: db2,
|
|
110200
|
+
localGameUrls: config5.localGameUrls,
|
|
110201
|
+
mintSkillStatusToken
|
|
110202
|
+
});
|
|
109988
110203
|
return {
|
|
109989
110204
|
assessmentAssets,
|
|
109990
110205
|
assessmentArtifacts,
|
|
@@ -109997,6 +110212,7 @@ function createPlatformServices(deps) {
|
|
|
109997
110212
|
deploymentState,
|
|
109998
110213
|
database,
|
|
109999
110214
|
seed,
|
|
110215
|
+
skillStatus,
|
|
110000
110216
|
timeback: timeback2,
|
|
110001
110217
|
timebackAdmin,
|
|
110002
110218
|
timebackAssessmentRuntime,
|
|
@@ -110015,6 +110231,7 @@ var init_platform2 = __esm(async () => {
|
|
|
110015
110231
|
init_releases_service();
|
|
110016
110232
|
init_secrets_service();
|
|
110017
110233
|
init_seed_service();
|
|
110234
|
+
init_skill_status_service();
|
|
110018
110235
|
init_upload_service();
|
|
110019
110236
|
await __promiseAll([
|
|
110020
110237
|
init_timeback_admin_service(),
|
|
@@ -110733,6 +110950,7 @@ function createServices(ctx) {
|
|
|
110733
110950
|
timebackClient: timeback2,
|
|
110734
110951
|
alerts: infra2.alerts,
|
|
110735
110952
|
mintPlatformServiceToken: auth2.mintPlatformServiceToken.bind(auth2),
|
|
110953
|
+
mintSkillStatusToken: auth2.mintSkillStatusToken.bind(auth2),
|
|
110736
110954
|
mintAssessmentPreparationReceipt: auth2.mintAssessmentPreparationReceipt.bind(auth2),
|
|
110737
110955
|
verifyAssessmentPreparationReceipt: auth2.verifyAssessmentPreparationReceipt.bind(auth2),
|
|
110738
110956
|
mintAssessmentToken: auth2.mintAssessmentToken.bind(auth2),
|
|
@@ -110781,6 +110999,17 @@ function workerKeyGrantsGameRead(key, slug2) {
|
|
|
110781
110999
|
function isGameWorkerKey(key) {
|
|
110782
111000
|
return Boolean(key?.name?.startsWith(GAME_WORKER_KEY_PREFIX));
|
|
110783
111001
|
}
|
|
111002
|
+
function gameWorkerKeySlug(key) {
|
|
111003
|
+
if (!isGameWorkerKey(key)) {
|
|
111004
|
+
return null;
|
|
111005
|
+
}
|
|
111006
|
+
const writes = (key?.permissions?.games ?? []).filter((scope) => scope.startsWith("write:"));
|
|
111007
|
+
if (writes.length !== 1) {
|
|
111008
|
+
return null;
|
|
111009
|
+
}
|
|
111010
|
+
const slug2 = writes[0].slice("write:".length);
|
|
111011
|
+
return slug2.length > 0 ? slug2 : null;
|
|
111012
|
+
}
|
|
110784
111013
|
function deniedWorkerKeyRequest(key, method, pathname) {
|
|
110785
111014
|
if (isDashboardWorkerKey(key) && !isAllowedDashboardWorkerRequest(method, pathname, key.permissions)) {
|
|
110786
111015
|
return DASHBOARD_WORKER_KEY_RESTRICTED;
|
|
@@ -111157,6 +111386,19 @@ var init_clients = __esm(async () => {
|
|
|
111157
111386
|
});
|
|
111158
111387
|
|
|
111159
111388
|
// src/infrastructure/api/providers/auth.provider.ts
|
|
111389
|
+
function mintSandboxServiceToken(audience, claims) {
|
|
111390
|
+
const now2 = Math.floor(Date.now() / 1000);
|
|
111391
|
+
const header = btoa(JSON.stringify({ alg: "none", kid: "sandbox", typ: "sandbox" }));
|
|
111392
|
+
const payload = btoa(JSON.stringify({
|
|
111393
|
+
iss: "playcademy-platform",
|
|
111394
|
+
aud: audience,
|
|
111395
|
+
...claims,
|
|
111396
|
+
iat: now2,
|
|
111397
|
+
exp: now2 + 60,
|
|
111398
|
+
jti: crypto.randomUUID()
|
|
111399
|
+
}));
|
|
111400
|
+
return `${header}.${payload}.sandbox`;
|
|
111401
|
+
}
|
|
111160
111402
|
function createSandboxAuthProvider() {
|
|
111161
111403
|
return {
|
|
111162
111404
|
async createApiKey(params) {
|
|
@@ -111228,18 +111470,10 @@ function createSandboxAuthProvider() {
|
|
|
111228
111470
|
}
|
|
111229
111471
|
},
|
|
111230
111472
|
async mintPlatformServiceToken(gameId, studentId) {
|
|
111231
|
-
|
|
111232
|
-
|
|
111233
|
-
|
|
111234
|
-
|
|
111235
|
-
aud: "playcademy-game-metrics",
|
|
111236
|
-
gameId,
|
|
111237
|
-
studentId,
|
|
111238
|
-
iat: now2,
|
|
111239
|
-
exp: now2 + 60,
|
|
111240
|
-
jti: crypto.randomUUID()
|
|
111241
|
-
}));
|
|
111242
|
-
return `${header}.${payload}.sandbox`;
|
|
111473
|
+
return mintSandboxServiceToken("playcademy-game-metrics", { gameId, studentId });
|
|
111474
|
+
},
|
|
111475
|
+
async mintSkillStatusToken(claims) {
|
|
111476
|
+
return mintSandboxServiceToken(SKILL_STATUS_TOKEN_AUDIENCE, { ...claims });
|
|
111243
111477
|
},
|
|
111244
111478
|
async mintAssessmentPreparationReceipt(plan) {
|
|
111245
111479
|
const now2 = Date.now();
|
|
@@ -111319,6 +111553,7 @@ function createSandboxAuthProvider() {
|
|
|
111319
111553
|
}
|
|
111320
111554
|
var sandboxApiKeys, sandboxAssessmentPreparationReceipts, ASSESSMENT_PREPARATION_RECEIPT_LIFETIME_MS, MAX_TOKEN_LIFETIME_SECONDS, sandboxAssessmentTokens;
|
|
111321
111555
|
var init_auth_provider = __esm(async () => {
|
|
111556
|
+
init_src();
|
|
111322
111557
|
init_src6();
|
|
111323
111558
|
init_constants();
|
|
111324
111559
|
await init_dist5();
|
|
@@ -167914,6 +168149,36 @@ var init_session_controller = __esm(() => {
|
|
|
167914
168149
|
});
|
|
167915
168150
|
});
|
|
167916
168151
|
|
|
168152
|
+
// ../api-core/src/controllers/skills.controller.ts
|
|
168153
|
+
async function getStatus4(ctx) {
|
|
168154
|
+
const requestingGameSlug = gameWorkerKeySlug(ctx.apiKey);
|
|
168155
|
+
if (!requestingGameSlug) {
|
|
168156
|
+
throw ApiError.forbidden("Skill status can only be requested by a deployed game worker");
|
|
168157
|
+
}
|
|
168158
|
+
const body2 = await parseRequestBody(ctx.request, SkillStatusRequestSchema);
|
|
168159
|
+
return ctx.services.skillStatus.getStatus({ ...body2, requestingGameSlug });
|
|
168160
|
+
}
|
|
168161
|
+
async function getStatusLocally(ctx) {
|
|
168162
|
+
await parseRequestBody(ctx.request, SkillStatusRequestSchema);
|
|
168163
|
+
return { supported: false, reason: "no_active_deployment" };
|
|
168164
|
+
}
|
|
168165
|
+
var SkillStatusRequestSchema, skills2;
|
|
168166
|
+
var init_skills_controller = __esm(() => {
|
|
168167
|
+
init_esm();
|
|
168168
|
+
init_src();
|
|
168169
|
+
init_errors2();
|
|
168170
|
+
init_utils7();
|
|
168171
|
+
SkillStatusRequestSchema = exports_external.object({
|
|
168172
|
+
game: exports_external.string().min(1),
|
|
168173
|
+
studentId: exports_external.string().min(1),
|
|
168174
|
+
skills: exports_external.array(exports_external.string()).min(1).max(SKILL_STATUS_MAX_SKILLS * 2)
|
|
168175
|
+
});
|
|
168176
|
+
skills2 = defineControllerNames("skills", {
|
|
168177
|
+
getStatus: requireAuth(getStatus4),
|
|
168178
|
+
getStatusLocally: requireAuth(getStatusLocally)
|
|
168179
|
+
});
|
|
168180
|
+
});
|
|
168181
|
+
|
|
167917
168182
|
// ../api-core/src/controllers/timeback.controller.ts
|
|
167918
168183
|
function parseQtiLibraryParams(searchParams) {
|
|
167919
168184
|
const requestedPage = Number(searchParams.get("page"));
|
|
@@ -168951,6 +169216,7 @@ var init_controllers = __esm(() => {
|
|
|
168951
169216
|
init_secrets_controller();
|
|
168952
169217
|
init_seed_controller();
|
|
168953
169218
|
init_session_controller();
|
|
169219
|
+
init_skills_controller();
|
|
168954
169220
|
init_timeback_controller();
|
|
168955
169221
|
init_upload_controller();
|
|
168956
169222
|
init_user_controller();
|
|
@@ -169764,6 +170030,16 @@ var init_leaderboard = __esm(async () => {
|
|
|
169764
170030
|
leaderboardRouter.get("/", handle2(leaderboard.getGlobalLeaderboard));
|
|
169765
170031
|
});
|
|
169766
170032
|
|
|
170033
|
+
// src/routes/platform/skills.ts
|
|
170034
|
+
var skillsRouter;
|
|
170035
|
+
var init_skills3 = __esm(async () => {
|
|
170036
|
+
init_dist7();
|
|
170037
|
+
init_controllers();
|
|
170038
|
+
await init_api3();
|
|
170039
|
+
skillsRouter = new Hono2;
|
|
170040
|
+
skillsRouter.post("/status", handle2(skills2.getStatusLocally));
|
|
170041
|
+
});
|
|
170042
|
+
|
|
169767
170043
|
// src/routes/integrations/timeback.ts
|
|
169768
170044
|
function hashCode(str) {
|
|
169769
170045
|
let hash2 = 0;
|
|
@@ -170112,6 +170388,7 @@ var exports_routes = {};
|
|
|
170112
170388
|
__export(exports_routes, {
|
|
170113
170389
|
usersRouter: () => usersRouter,
|
|
170114
170390
|
timebackRouter: () => timebackRouter,
|
|
170391
|
+
skillsRouter: () => skillsRouter,
|
|
170115
170392
|
manifestRouter: () => manifestRouter,
|
|
170116
170393
|
ltiRouter: () => ltiRouter,
|
|
170117
170394
|
leaderboardRouter: () => leaderboardRouter,
|
|
@@ -170128,6 +170405,7 @@ var init_routes = __esm(async () => {
|
|
|
170128
170405
|
init_users(),
|
|
170129
170406
|
init_games2(),
|
|
170130
170407
|
init_leaderboard(),
|
|
170408
|
+
init_skills3(),
|
|
170131
170409
|
init_timeback8(),
|
|
170132
170410
|
init_lti()
|
|
170133
170411
|
]);
|
|
@@ -170143,6 +170421,7 @@ async function registerRoutes(app) {
|
|
|
170143
170421
|
leaderboardRouter: leaderboardRouter2,
|
|
170144
170422
|
ltiRouter: ltiRouter2,
|
|
170145
170423
|
manifestRouter: manifestRouter2,
|
|
170424
|
+
skillsRouter: skillsRouter2,
|
|
170146
170425
|
timebackRouter: timebackRouter2,
|
|
170147
170426
|
usersRouter: usersRouter2
|
|
170148
170427
|
} = await init_routes().then(() => exports_routes);
|
|
@@ -170151,6 +170430,7 @@ async function registerRoutes(app) {
|
|
|
170151
170430
|
app.route("/api/users", usersRouter2);
|
|
170152
170431
|
app.route("/api/games", gamesRouter2);
|
|
170153
170432
|
app.route("/api/leaderboard", leaderboardRouter2);
|
|
170433
|
+
app.route("/api/skills", skillsRouter2);
|
|
170154
170434
|
app.route("/api/dev", devRouter2);
|
|
170155
170435
|
app.route("/api/timeback", timebackRouter2);
|
|
170156
170436
|
app.route("/api/lti", ltiRouter2);
|
package/dist/constants.js
CHANGED
|
@@ -97,7 +97,17 @@ var init_platform = __esm(() => {
|
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
// ../constants/src/skills.ts
|
|
100
|
-
var
|
|
100
|
+
var SKILL_STATUS_ROUTE = "/__playcademy/skills/status", SKILL_STATUS_TOKEN_AUDIENCE = "playcademy-game-skill-status", SKILL_STATUS_MAX_SKILLS = 50, SKILL_STATUSES, SKILL_MASTERY_SOURCES;
|
|
101
|
+
var init_skills = __esm(() => {
|
|
102
|
+
SKILL_STATUSES = [
|
|
103
|
+
"locked",
|
|
104
|
+
"unlocked",
|
|
105
|
+
"in_progress",
|
|
106
|
+
"mastered",
|
|
107
|
+
"unknown_skill"
|
|
108
|
+
];
|
|
109
|
+
SKILL_MASTERY_SOURCES = ["assessment", "placement", "test_out", "manual"];
|
|
110
|
+
});
|
|
101
111
|
|
|
102
112
|
// ../constants/src/system.ts
|
|
103
113
|
var PLATFORM_TIMEZONE = "America/New_York";
|
package/dist/server.js
CHANGED
|
@@ -281,7 +281,17 @@ var init_platform = __esm(() => {
|
|
|
281
281
|
});
|
|
282
282
|
|
|
283
283
|
// ../constants/src/skills.ts
|
|
284
|
-
var
|
|
284
|
+
var SKILL_STATUS_ROUTE = "/__playcademy/skills/status", SKILL_STATUS_TOKEN_AUDIENCE = "playcademy-game-skill-status", SKILL_STATUS_MAX_SKILLS = 50, SKILL_STATUSES, SKILL_MASTERY_SOURCES;
|
|
285
|
+
var init_skills = __esm(() => {
|
|
286
|
+
SKILL_STATUSES = [
|
|
287
|
+
"locked",
|
|
288
|
+
"unlocked",
|
|
289
|
+
"in_progress",
|
|
290
|
+
"mastered",
|
|
291
|
+
"unknown_skill"
|
|
292
|
+
];
|
|
293
|
+
SKILL_MASTERY_SOURCES = ["assessment", "placement", "test_out", "manual"];
|
|
294
|
+
});
|
|
285
295
|
|
|
286
296
|
// ../constants/src/system.ts
|
|
287
297
|
var PLATFORM_TIMEZONE = "America/New_York";
|
|
@@ -1131,7 +1141,7 @@ var package_default;
|
|
|
1131
1141
|
var init_package = __esm(() => {
|
|
1132
1142
|
package_default = {
|
|
1133
1143
|
name: "@playcademy/sandbox",
|
|
1134
|
-
version: "0.10.2-beta.
|
|
1144
|
+
version: "0.10.2-beta.3",
|
|
1135
1145
|
description: "Local development server for Playcademy game development",
|
|
1136
1146
|
type: "module",
|
|
1137
1147
|
exports: {
|
|
@@ -30008,6 +30018,23 @@ function generateDeploymentHash(code) {
|
|
|
30008
30018
|
function computeDeployPayloadFingerprint(payload) {
|
|
30009
30019
|
return sha256Hex(canonicalJson2(payload));
|
|
30010
30020
|
}
|
|
30021
|
+
function toActiveDeploymentRow(record) {
|
|
30022
|
+
return {
|
|
30023
|
+
gameId: record.gameId,
|
|
30024
|
+
deploymentId: record.deploymentId,
|
|
30025
|
+
provider: "cloudflare",
|
|
30026
|
+
target: record.target,
|
|
30027
|
+
url: record.url,
|
|
30028
|
+
codeHash: record.codeHash,
|
|
30029
|
+
schemaHash: record.schemaHash ?? null,
|
|
30030
|
+
schemaFingerprint: record.schemaFingerprint ?? null,
|
|
30031
|
+
timeTravelBookmark: record.timeTravelBookmark ?? null,
|
|
30032
|
+
bookmarkCapturedAt: record.bookmarkCapturedAt ?? null,
|
|
30033
|
+
resources: record.resources,
|
|
30034
|
+
isActive: true,
|
|
30035
|
+
deployJobId: record.deployJobId ?? null
|
|
30036
|
+
};
|
|
30037
|
+
}
|
|
30011
30038
|
var GAME_WORKER_KEY_PREFIX = "game-worker-", DASHBOARD_WORKER_KEY_PREFIX = "dash-worker-";
|
|
30012
30039
|
var init_deployment_util = __esm(() => {
|
|
30013
30040
|
init_drizzle_orm();
|
|
@@ -37031,24 +37058,9 @@ class DeployService {
|
|
|
37031
37058
|
}
|
|
37032
37059
|
}
|
|
37033
37060
|
async saveDeployment(record) {
|
|
37034
|
-
|
|
37035
|
-
await db2.transaction(async (tx) => {
|
|
37061
|
+
await this.deps.db.transaction(async (tx) => {
|
|
37036
37062
|
await tx.update(gameDeployments).set({ isActive: false }).where(and(eq(gameDeployments.gameId, record.gameId), eq(gameDeployments.target, record.target)));
|
|
37037
|
-
await tx.insert(gameDeployments).values(
|
|
37038
|
-
gameId: record.gameId,
|
|
37039
|
-
deploymentId: record.deploymentId,
|
|
37040
|
-
provider: "cloudflare",
|
|
37041
|
-
target: record.target,
|
|
37042
|
-
url: record.url,
|
|
37043
|
-
codeHash: record.codeHash,
|
|
37044
|
-
schemaHash: record.schemaHash ?? null,
|
|
37045
|
-
schemaFingerprint: record.schemaFingerprint ?? null,
|
|
37046
|
-
timeTravelBookmark: record.timeTravelBookmark ?? null,
|
|
37047
|
-
bookmarkCapturedAt: record.bookmarkCapturedAt ?? null,
|
|
37048
|
-
resources: record.resources,
|
|
37049
|
-
isActive: true,
|
|
37050
|
-
deployJobId: record.deployJobId ?? null
|
|
37051
|
-
});
|
|
37063
|
+
await tx.insert(gameDeployments).values(toActiveDeploymentRow(record));
|
|
37052
37064
|
});
|
|
37053
37065
|
}
|
|
37054
37066
|
async notifyDeploymentFailure(failure) {
|
|
@@ -46880,6 +46892,39 @@ function publishableRepositoryUrl(value) {
|
|
|
46880
46892
|
return value;
|
|
46881
46893
|
}
|
|
46882
46894
|
|
|
46895
|
+
// ../api-core/src/utils/releases.util.ts
|
|
46896
|
+
function toRelease(row) {
|
|
46897
|
+
return {
|
|
46898
|
+
slug: row.slug,
|
|
46899
|
+
url: row.url,
|
|
46900
|
+
source: normalizeDeploySource(row.source),
|
|
46901
|
+
deployedAt: row.deployedAt.toISOString()
|
|
46902
|
+
};
|
|
46903
|
+
}
|
|
46904
|
+
function toGameReleaseResponse(rows, history) {
|
|
46905
|
+
const releases = rows.map(toRelease);
|
|
46906
|
+
const current = releases[0] ?? null;
|
|
46907
|
+
return history === undefined ? { current } : { current, history: releases };
|
|
46908
|
+
}
|
|
46909
|
+
function normalizeDeploySource(value) {
|
|
46910
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
46911
|
+
return null;
|
|
46912
|
+
}
|
|
46913
|
+
const raw = value;
|
|
46914
|
+
const repository = nullableString(raw.repository);
|
|
46915
|
+
return {
|
|
46916
|
+
repository: repository === null ? null : publishableRepositoryUrl(repository),
|
|
46917
|
+
commit: nullableString(raw.commit),
|
|
46918
|
+
branch: nullableString(raw.branch),
|
|
46919
|
+
dirty: raw.dirty === true,
|
|
46920
|
+
externalBuild: raw.externalBuild === true
|
|
46921
|
+
};
|
|
46922
|
+
}
|
|
46923
|
+
function nullableString(value) {
|
|
46924
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
46925
|
+
}
|
|
46926
|
+
var init_releases_util = () => {};
|
|
46927
|
+
|
|
46883
46928
|
// ../api-core/src/services/releases.service.ts
|
|
46884
46929
|
class ReleasesService {
|
|
46885
46930
|
deps;
|
|
@@ -46904,9 +46949,7 @@ class ReleasesService {
|
|
|
46904
46949
|
"app.releases.history_requested": history ?? 0,
|
|
46905
46950
|
"app.releases.history_rows": rows.length
|
|
46906
46951
|
});
|
|
46907
|
-
|
|
46908
|
-
const current = releases[0] ?? null;
|
|
46909
|
-
return history === undefined ? { current } : { current, history: releases };
|
|
46952
|
+
return toGameReleaseResponse(rows, history);
|
|
46910
46953
|
}
|
|
46911
46954
|
}
|
|
46912
46955
|
function releaseColumns() {
|
|
@@ -46923,36 +46966,12 @@ function listedGame() {
|
|
|
46923
46966
|
function activeWorkerDeployment() {
|
|
46924
46967
|
return and(eq(gameDeployments.isActive, true), eq(gameDeployments.target, "game"));
|
|
46925
46968
|
}
|
|
46926
|
-
function toRelease(row) {
|
|
46927
|
-
return {
|
|
46928
|
-
slug: row.slug,
|
|
46929
|
-
url: row.url,
|
|
46930
|
-
source: normalizeSource(row.source),
|
|
46931
|
-
deployedAt: row.deployedAt.toISOString()
|
|
46932
|
-
};
|
|
46933
|
-
}
|
|
46934
|
-
function normalizeSource(value) {
|
|
46935
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
46936
|
-
return null;
|
|
46937
|
-
}
|
|
46938
|
-
const raw = value;
|
|
46939
|
-
const repository = nullableString(raw.repository);
|
|
46940
|
-
return {
|
|
46941
|
-
repository: repository === null ? null : publishableRepositoryUrl(repository),
|
|
46942
|
-
commit: nullableString(raw.commit),
|
|
46943
|
-
branch: nullableString(raw.branch),
|
|
46944
|
-
dirty: raw.dirty === true,
|
|
46945
|
-
externalBuild: raw.externalBuild === true
|
|
46946
|
-
};
|
|
46947
|
-
}
|
|
46948
|
-
function nullableString(value) {
|
|
46949
|
-
return typeof value === "string" && value.length > 0 ? value : null;
|
|
46950
|
-
}
|
|
46951
46969
|
var init_releases_service = __esm(() => {
|
|
46952
46970
|
init_drizzle_orm();
|
|
46953
46971
|
init_tables_index();
|
|
46954
46972
|
init_spans();
|
|
46955
46973
|
init_errors2();
|
|
46974
|
+
init_releases_util();
|
|
46956
46975
|
});
|
|
46957
46976
|
|
|
46958
46977
|
// ../api-core/src/services/secrets.service.ts
|
|
@@ -47408,6 +47427,226 @@ var init_seed_service = __esm(() => {
|
|
|
47408
47427
|
init_deployment_util();
|
|
47409
47428
|
});
|
|
47410
47429
|
|
|
47430
|
+
// ../api-core/src/utils/game-worker-call.util.ts
|
|
47431
|
+
function resolveGameWorkerBaseUrl(localGameUrls, slug, deployedUrl) {
|
|
47432
|
+
return (slug ? localGameUrls[slug] : undefined) ?? deployedUrl;
|
|
47433
|
+
}
|
|
47434
|
+
async function callGameWorker(url, token, options) {
|
|
47435
|
+
const controller = new AbortController;
|
|
47436
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
47437
|
+
try {
|
|
47438
|
+
const response = await fetch(url, {
|
|
47439
|
+
method: "GET",
|
|
47440
|
+
headers: {
|
|
47441
|
+
Accept: "application/json",
|
|
47442
|
+
Authorization: `Bearer ${token}`
|
|
47443
|
+
},
|
|
47444
|
+
signal: controller.signal
|
|
47445
|
+
});
|
|
47446
|
+
const body2 = await response.text();
|
|
47447
|
+
return {
|
|
47448
|
+
ok: true,
|
|
47449
|
+
response: new Response(NULL_BODY_STATUSES.has(response.status) ? null : body2, {
|
|
47450
|
+
status: response.status,
|
|
47451
|
+
headers: response.headers
|
|
47452
|
+
})
|
|
47453
|
+
};
|
|
47454
|
+
} catch (error) {
|
|
47455
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
47456
|
+
return { ok: false, reason: "timeout", details: options.timeoutMessage };
|
|
47457
|
+
}
|
|
47458
|
+
return { ok: false, reason: "fetch_failed", details: errorMessage(error) };
|
|
47459
|
+
} finally {
|
|
47460
|
+
clearTimeout(timeout);
|
|
47461
|
+
}
|
|
47462
|
+
}
|
|
47463
|
+
var NULL_BODY_STATUSES;
|
|
47464
|
+
var init_game_worker_call_util = __esm(() => {
|
|
47465
|
+
NULL_BODY_STATUSES = new Set([101, 204, 205, 304]);
|
|
47466
|
+
});
|
|
47467
|
+
|
|
47468
|
+
// ../utils/src/skills.ts
|
|
47469
|
+
function normalizeSkillId(skill) {
|
|
47470
|
+
return skill.trim().toLowerCase();
|
|
47471
|
+
}
|
|
47472
|
+
function normalizeSkillIds(skills2, max) {
|
|
47473
|
+
const normalized = [...new Set(skills2.map(normalizeSkillId).filter((skill) => skill.length > 0))];
|
|
47474
|
+
if (normalized.length === 0) {
|
|
47475
|
+
return { ok: false, error: "At least one skill is required" };
|
|
47476
|
+
}
|
|
47477
|
+
if (normalized.length > max) {
|
|
47478
|
+
return { ok: false, error: `At most ${max} skills can be requested at once` };
|
|
47479
|
+
}
|
|
47480
|
+
return { ok: true, skills: normalized };
|
|
47481
|
+
}
|
|
47482
|
+
|
|
47483
|
+
// ../edge-play/src/game/lib/skills.ts
|
|
47484
|
+
function isValidIsoDate(value) {
|
|
47485
|
+
if (typeof value !== "string" || !ISO_DATE_PATTERN.test(value)) {
|
|
47486
|
+
return false;
|
|
47487
|
+
}
|
|
47488
|
+
const [year, month, day] = value.slice(0, 10).split("-").map(Number);
|
|
47489
|
+
const date3 = new Date(Date.UTC(year, month - 1, day));
|
|
47490
|
+
return !isNaN(Date.parse(value)) && date3.getUTCFullYear() === year && date3.getUTCMonth() === month - 1 && date3.getUTCDate() === day;
|
|
47491
|
+
}
|
|
47492
|
+
function normalizeResult(result) {
|
|
47493
|
+
if (!isRecord(result)) {
|
|
47494
|
+
return null;
|
|
47495
|
+
}
|
|
47496
|
+
if (typeof result.skill !== "string" || !STATUSES.has(result.status)) {
|
|
47497
|
+
return null;
|
|
47498
|
+
}
|
|
47499
|
+
const status = result.status;
|
|
47500
|
+
if (result.source !== undefined) {
|
|
47501
|
+
if (status !== "mastered" || !MASTERY_SOURCES.has(result.source)) {
|
|
47502
|
+
return null;
|
|
47503
|
+
}
|
|
47504
|
+
}
|
|
47505
|
+
if (result.asOf !== undefined && !isValidIsoDate(result.asOf)) {
|
|
47506
|
+
return null;
|
|
47507
|
+
}
|
|
47508
|
+
return {
|
|
47509
|
+
skill: normalizeSkillId(result.skill),
|
|
47510
|
+
status,
|
|
47511
|
+
...result.source === undefined ? {} : { source: result.source },
|
|
47512
|
+
...result.asOf === undefined ? {} : { asOf: result.asOf }
|
|
47513
|
+
};
|
|
47514
|
+
}
|
|
47515
|
+
function validateSkillStatusResponse(value, studentId, askedSkills) {
|
|
47516
|
+
if (!isRecord(value)) {
|
|
47517
|
+
return null;
|
|
47518
|
+
}
|
|
47519
|
+
const response = value;
|
|
47520
|
+
if (response.studentId !== studentId || !Array.isArray(response.results)) {
|
|
47521
|
+
return null;
|
|
47522
|
+
}
|
|
47523
|
+
const asked = new Set(askedSkills);
|
|
47524
|
+
const results = new Map;
|
|
47525
|
+
for (const raw of response.results) {
|
|
47526
|
+
const result = normalizeResult(raw);
|
|
47527
|
+
if (!result || !asked.has(result.skill) || results.has(result.skill)) {
|
|
47528
|
+
return null;
|
|
47529
|
+
}
|
|
47530
|
+
results.set(result.skill, result);
|
|
47531
|
+
}
|
|
47532
|
+
return results.size === asked.size ? results : null;
|
|
47533
|
+
}
|
|
47534
|
+
var STATUSES, MASTERY_SOURCES, ISO_DATE_PATTERN;
|
|
47535
|
+
var init_skills2 = __esm(() => {
|
|
47536
|
+
init_src();
|
|
47537
|
+
STATUSES = new Set(SKILL_STATUSES);
|
|
47538
|
+
MASTERY_SOURCES = new Set(SKILL_MASTERY_SOURCES);
|
|
47539
|
+
ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:\d{2}))?$/;
|
|
47540
|
+
});
|
|
47541
|
+
|
|
47542
|
+
// ../api-core/src/utils/skill-status.util.ts
|
|
47543
|
+
function normalizeSkillStatusRequest(skills2) {
|
|
47544
|
+
const normalized = normalizeSkillIds(skills2, SKILL_STATUS_MAX_SKILLS);
|
|
47545
|
+
if (!normalized.ok) {
|
|
47546
|
+
throw new ValidationError(normalized.error);
|
|
47547
|
+
}
|
|
47548
|
+
return normalized.skills;
|
|
47549
|
+
}
|
|
47550
|
+
function buildSkillStatusUrl(baseUrl, skills2) {
|
|
47551
|
+
const url = new URL(SKILL_STATUS_ROUTE, baseUrl);
|
|
47552
|
+
for (const skill of skills2) {
|
|
47553
|
+
url.searchParams.append("skill", skill);
|
|
47554
|
+
}
|
|
47555
|
+
return url;
|
|
47556
|
+
}
|
|
47557
|
+
function pickSkillStatusGames(rows, requestingSlug, answeringSlug) {
|
|
47558
|
+
const requestingGame = rows.find((row) => row.slug === requestingSlug);
|
|
47559
|
+
const answeringGame = rows.find((row) => row.slug === answeringSlug);
|
|
47560
|
+
if (!requestingGame) {
|
|
47561
|
+
throw new NotFoundError("Game", requestingSlug);
|
|
47562
|
+
}
|
|
47563
|
+
if (!answeringGame) {
|
|
47564
|
+
throw new NotFoundError("Game", answeringSlug);
|
|
47565
|
+
}
|
|
47566
|
+
return { requestingGame, answeringGame };
|
|
47567
|
+
}
|
|
47568
|
+
async function classifySkillStatusResponse(response, studentId, skills2) {
|
|
47569
|
+
if (response.status === 404) {
|
|
47570
|
+
return { supported: false, reason: "not_implemented" };
|
|
47571
|
+
}
|
|
47572
|
+
if (!response.ok) {
|
|
47573
|
+
return { supported: false, reason: "fetch_failed" };
|
|
47574
|
+
}
|
|
47575
|
+
let json3;
|
|
47576
|
+
try {
|
|
47577
|
+
json3 = JSON.parse(await response.text());
|
|
47578
|
+
} catch {
|
|
47579
|
+
return { supported: false, reason: "not_implemented" };
|
|
47580
|
+
}
|
|
47581
|
+
const answered = validateSkillStatusResponse(json3, studentId, skills2);
|
|
47582
|
+
if (!answered) {
|
|
47583
|
+
return { supported: false, reason: "invalid_response" };
|
|
47584
|
+
}
|
|
47585
|
+
return { supported: true, results: skills2.map((skill) => answered.get(skill)) };
|
|
47586
|
+
}
|
|
47587
|
+
var init_skill_status_util = __esm(() => {
|
|
47588
|
+
init_src();
|
|
47589
|
+
init_skills2();
|
|
47590
|
+
init_errors2();
|
|
47591
|
+
});
|
|
47592
|
+
|
|
47593
|
+
// ../api-core/src/services/skill-status.service.ts
|
|
47594
|
+
class SkillStatusService {
|
|
47595
|
+
deps;
|
|
47596
|
+
constructor(deps) {
|
|
47597
|
+
this.deps = deps;
|
|
47598
|
+
}
|
|
47599
|
+
async getStatus(input) {
|
|
47600
|
+
const skills2 = normalizeSkillStatusRequest(input.skills);
|
|
47601
|
+
setAttributes({
|
|
47602
|
+
"app.skill_status.requesting_game": input.requestingGameSlug,
|
|
47603
|
+
"app.skill_status.answering_game": input.game,
|
|
47604
|
+
"app.skill_status.skill_count": skills2.length
|
|
47605
|
+
});
|
|
47606
|
+
const rows = await this.deps.db.select({ id: games.id, slug: games.slug }).from(games).where(inArray(games.slug, [input.requestingGameSlug, input.game]));
|
|
47607
|
+
const { requestingGame, answeringGame } = pickSkillStatusGames(rows, input.requestingGameSlug, input.game);
|
|
47608
|
+
const deployment = await this.deps.db.query.gameDeployments.findFirst({
|
|
47609
|
+
where: activeDeploymentWhere(answeringGame.id),
|
|
47610
|
+
columns: { url: true }
|
|
47611
|
+
});
|
|
47612
|
+
const baseUrl = resolveGameWorkerBaseUrl(this.deps.localGameUrls, answeringGame.slug, deployment?.url);
|
|
47613
|
+
if (!baseUrl) {
|
|
47614
|
+
return recordResult({ supported: false, reason: "no_active_deployment" });
|
|
47615
|
+
}
|
|
47616
|
+
const token = await this.deps.mintSkillStatusToken({
|
|
47617
|
+
gameId: answeringGame.id,
|
|
47618
|
+
studentId: input.studentId,
|
|
47619
|
+
requestingGame
|
|
47620
|
+
});
|
|
47621
|
+
const call = await callGameWorker(buildSkillStatusUrl(baseUrl, skills2), token, {
|
|
47622
|
+
timeoutMs: SKILL_STATUS_TIMEOUT_MS,
|
|
47623
|
+
timeoutMessage: "Skill status request timed out"
|
|
47624
|
+
});
|
|
47625
|
+
if (!call.ok) {
|
|
47626
|
+
setAttributes({ "app.skill_status.failure_details": call.details });
|
|
47627
|
+
return recordResult({ supported: false, reason: call.reason });
|
|
47628
|
+
}
|
|
47629
|
+
setAttributes({ "app.skill_status.http_status": call.response.status });
|
|
47630
|
+
return recordResult(await classifySkillStatusResponse(call.response, input.studentId, skills2));
|
|
47631
|
+
}
|
|
47632
|
+
}
|
|
47633
|
+
function recordResult(result) {
|
|
47634
|
+
setAttributes(result.supported ? {
|
|
47635
|
+
"app.skill_status.supported": true,
|
|
47636
|
+
"app.skill_status.unknown_skill_count": result.results.filter((entry) => entry.status === "unknown_skill").length
|
|
47637
|
+
} : { "app.skill_status.supported": false, "app.skill_status.reason": result.reason });
|
|
47638
|
+
return result;
|
|
47639
|
+
}
|
|
47640
|
+
var SKILL_STATUS_TIMEOUT_MS = 3000;
|
|
47641
|
+
var init_skill_status_service = __esm(() => {
|
|
47642
|
+
init_drizzle_orm();
|
|
47643
|
+
init_helpers_index();
|
|
47644
|
+
init_tables_index();
|
|
47645
|
+
init_spans();
|
|
47646
|
+
init_game_worker_call_util();
|
|
47647
|
+
init_skill_status_util();
|
|
47648
|
+
});
|
|
47649
|
+
|
|
47411
47650
|
// ../../node_modules/.bun/drizzle-zod@0.7.1+3ad0422b77d1a05b/node_modules/drizzle-zod/index.mjs
|
|
47412
47651
|
function isColumnType(column2, columnTypes) {
|
|
47413
47652
|
return columnTypes.includes(column2.columnType);
|
|
@@ -98257,12 +98496,6 @@ class TimebackAdminService {
|
|
|
98257
98496
|
grade: integration.grade
|
|
98258
98497
|
};
|
|
98259
98498
|
}
|
|
98260
|
-
resolveGameMetricsBaseUrl(slug2, deployedUrl) {
|
|
98261
|
-
if (!slug2) {
|
|
98262
|
-
return deployedUrl;
|
|
98263
|
-
}
|
|
98264
|
-
return this.deps.config.localGameUrls[slug2] ?? deployedUrl;
|
|
98265
|
-
}
|
|
98266
98499
|
static resolveGameMetricsUrl(baseUrl, runIds) {
|
|
98267
98500
|
try {
|
|
98268
98501
|
const url4 = new URL("/__playcademy/metrics", baseUrl);
|
|
@@ -98296,9 +98529,6 @@ class TimebackAdminService {
|
|
|
98296
98529
|
}
|
|
98297
98530
|
return chunks;
|
|
98298
98531
|
}
|
|
98299
|
-
static isAbortError(error88) {
|
|
98300
|
-
return error88 instanceof Error && error88.name === "AbortError";
|
|
98301
|
-
}
|
|
98302
98532
|
static roundXpToTenths(value) {
|
|
98303
98533
|
const rounded = Math.round(value * TimebackAdminService.XP_PRECISION_FACTOR) / TimebackAdminService.XP_PRECISION_FACTOR;
|
|
98304
98534
|
return Object.is(rounded, -0) ? 0 : rounded;
|
|
@@ -98936,7 +99166,7 @@ class TimebackAdminService {
|
|
|
98936
99166
|
if (enrollmentsByCourseId.size === 0) {
|
|
98937
99167
|
throw new NotFoundError("Student enrollment", `${timebackId}:${gameId}`);
|
|
98938
99168
|
}
|
|
98939
|
-
const metricsBaseUrl = this.
|
|
99169
|
+
const metricsBaseUrl = resolveGameWorkerBaseUrl(this.deps.config.localGameUrls, game2?.slug, deployment?.url);
|
|
98940
99170
|
if (!metricsBaseUrl) {
|
|
98941
99171
|
setAttributes({
|
|
98942
99172
|
"app.timeback.metrics.supported": false,
|
|
@@ -98959,40 +99189,18 @@ class TimebackAdminService {
|
|
|
98959
99189
|
};
|
|
98960
99190
|
}
|
|
98961
99191
|
const token = await this.deps.mintPlatformServiceToken(gameId, targetUser.id);
|
|
98962
|
-
const
|
|
98963
|
-
|
|
98964
|
-
|
|
98965
|
-
|
|
98966
|
-
|
|
98967
|
-
method: "GET",
|
|
98968
|
-
headers: {
|
|
98969
|
-
Accept: "application/json",
|
|
98970
|
-
Authorization: `Bearer ${token}`
|
|
98971
|
-
},
|
|
98972
|
-
signal: controller.signal
|
|
98973
|
-
});
|
|
98974
|
-
} catch (error88) {
|
|
98975
|
-
const timedOut = TimebackAdminService.isAbortError(error88);
|
|
98976
|
-
let details;
|
|
98977
|
-
if (timedOut) {
|
|
98978
|
-
details = "Game metrics request timed out";
|
|
98979
|
-
} else if (error88 instanceof Error) {
|
|
98980
|
-
details = error88.message;
|
|
98981
|
-
} else {
|
|
98982
|
-
details = String(error88);
|
|
98983
|
-
}
|
|
99192
|
+
const call = await callGameWorker(metricsUrl, token, {
|
|
99193
|
+
timeoutMs: options?.timeoutMs ?? TimebackAdminService.GAME_METRICS_FETCH_TIMEOUT_MS,
|
|
99194
|
+
timeoutMessage: "Game metrics request timed out"
|
|
99195
|
+
});
|
|
99196
|
+
if (!call.ok) {
|
|
98984
99197
|
setAttributes({
|
|
98985
99198
|
"app.timeback.metrics.supported": false,
|
|
98986
|
-
"app.timeback.metrics.reason":
|
|
99199
|
+
"app.timeback.metrics.reason": call.reason
|
|
98987
99200
|
});
|
|
98988
|
-
return {
|
|
98989
|
-
supported: false,
|
|
98990
|
-
reason: timedOut ? "timeout" : "fetch_failed",
|
|
98991
|
-
details
|
|
98992
|
-
};
|
|
98993
|
-
} finally {
|
|
98994
|
-
clearTimeout(timeout);
|
|
99201
|
+
return { supported: false, reason: call.reason, details: call.details };
|
|
98995
99202
|
}
|
|
99203
|
+
const { response } = call;
|
|
98996
99204
|
const result = await classifyGameMetricsResponse(response, { gameId, timebackId });
|
|
98997
99205
|
setAttributes({
|
|
98998
99206
|
"app.timeback.metrics.supported": result.supported,
|
|
@@ -99990,6 +100198,7 @@ var init_timeback_admin_service = __esm(async () => {
|
|
|
99990
100198
|
init_src9();
|
|
99991
100199
|
init_timeback3();
|
|
99992
100200
|
init_errors2();
|
|
100201
|
+
init_game_worker_call_util();
|
|
99993
100202
|
init_timeback_admin_metrics_util();
|
|
99994
100203
|
init_timeback_admin_util();
|
|
99995
100204
|
init_timeback_discrepancy_queue_util();
|
|
@@ -109901,6 +110110,7 @@ function createPlatformServices(deps) {
|
|
|
109901
110110
|
timebackClient,
|
|
109902
110111
|
alerts,
|
|
109903
110112
|
mintPlatformServiceToken,
|
|
110113
|
+
mintSkillStatusToken,
|
|
109904
110114
|
mintAssessmentPreparationReceipt,
|
|
109905
110115
|
verifyAssessmentPreparationReceipt,
|
|
109906
110116
|
mintAssessmentToken,
|
|
@@ -109984,6 +110194,11 @@ function createPlatformServices(deps) {
|
|
|
109984
110194
|
mintAssessmentToken,
|
|
109985
110195
|
verifyAssessmentToken
|
|
109986
110196
|
});
|
|
110197
|
+
const skillStatus = new SkillStatusService({
|
|
110198
|
+
db: db2,
|
|
110199
|
+
localGameUrls: config5.localGameUrls,
|
|
110200
|
+
mintSkillStatusToken
|
|
110201
|
+
});
|
|
109987
110202
|
return {
|
|
109988
110203
|
assessmentAssets,
|
|
109989
110204
|
assessmentArtifacts,
|
|
@@ -109996,6 +110211,7 @@ function createPlatformServices(deps) {
|
|
|
109996
110211
|
deploymentState,
|
|
109997
110212
|
database,
|
|
109998
110213
|
seed,
|
|
110214
|
+
skillStatus,
|
|
109999
110215
|
timeback: timeback2,
|
|
110000
110216
|
timebackAdmin,
|
|
110001
110217
|
timebackAssessmentRuntime,
|
|
@@ -110014,6 +110230,7 @@ var init_platform2 = __esm(async () => {
|
|
|
110014
110230
|
init_releases_service();
|
|
110015
110231
|
init_secrets_service();
|
|
110016
110232
|
init_seed_service();
|
|
110233
|
+
init_skill_status_service();
|
|
110017
110234
|
init_upload_service();
|
|
110018
110235
|
await __promiseAll([
|
|
110019
110236
|
init_timeback_admin_service(),
|
|
@@ -110732,6 +110949,7 @@ function createServices(ctx) {
|
|
|
110732
110949
|
timebackClient: timeback2,
|
|
110733
110950
|
alerts: infra2.alerts,
|
|
110734
110951
|
mintPlatformServiceToken: auth2.mintPlatformServiceToken.bind(auth2),
|
|
110952
|
+
mintSkillStatusToken: auth2.mintSkillStatusToken.bind(auth2),
|
|
110735
110953
|
mintAssessmentPreparationReceipt: auth2.mintAssessmentPreparationReceipt.bind(auth2),
|
|
110736
110954
|
verifyAssessmentPreparationReceipt: auth2.verifyAssessmentPreparationReceipt.bind(auth2),
|
|
110737
110955
|
mintAssessmentToken: auth2.mintAssessmentToken.bind(auth2),
|
|
@@ -110780,6 +110998,17 @@ function workerKeyGrantsGameRead(key, slug2) {
|
|
|
110780
110998
|
function isGameWorkerKey(key) {
|
|
110781
110999
|
return Boolean(key?.name?.startsWith(GAME_WORKER_KEY_PREFIX));
|
|
110782
111000
|
}
|
|
111001
|
+
function gameWorkerKeySlug(key) {
|
|
111002
|
+
if (!isGameWorkerKey(key)) {
|
|
111003
|
+
return null;
|
|
111004
|
+
}
|
|
111005
|
+
const writes = (key?.permissions?.games ?? []).filter((scope) => scope.startsWith("write:"));
|
|
111006
|
+
if (writes.length !== 1) {
|
|
111007
|
+
return null;
|
|
111008
|
+
}
|
|
111009
|
+
const slug2 = writes[0].slice("write:".length);
|
|
111010
|
+
return slug2.length > 0 ? slug2 : null;
|
|
111011
|
+
}
|
|
110783
111012
|
function deniedWorkerKeyRequest(key, method, pathname) {
|
|
110784
111013
|
if (isDashboardWorkerKey(key) && !isAllowedDashboardWorkerRequest(method, pathname, key.permissions)) {
|
|
110785
111014
|
return DASHBOARD_WORKER_KEY_RESTRICTED;
|
|
@@ -111156,6 +111385,19 @@ var init_clients = __esm(async () => {
|
|
|
111156
111385
|
});
|
|
111157
111386
|
|
|
111158
111387
|
// src/infrastructure/api/providers/auth.provider.ts
|
|
111388
|
+
function mintSandboxServiceToken(audience, claims) {
|
|
111389
|
+
const now2 = Math.floor(Date.now() / 1000);
|
|
111390
|
+
const header = btoa(JSON.stringify({ alg: "none", kid: "sandbox", typ: "sandbox" }));
|
|
111391
|
+
const payload = btoa(JSON.stringify({
|
|
111392
|
+
iss: "playcademy-platform",
|
|
111393
|
+
aud: audience,
|
|
111394
|
+
...claims,
|
|
111395
|
+
iat: now2,
|
|
111396
|
+
exp: now2 + 60,
|
|
111397
|
+
jti: crypto.randomUUID()
|
|
111398
|
+
}));
|
|
111399
|
+
return `${header}.${payload}.sandbox`;
|
|
111400
|
+
}
|
|
111159
111401
|
function createSandboxAuthProvider() {
|
|
111160
111402
|
return {
|
|
111161
111403
|
async createApiKey(params) {
|
|
@@ -111227,18 +111469,10 @@ function createSandboxAuthProvider() {
|
|
|
111227
111469
|
}
|
|
111228
111470
|
},
|
|
111229
111471
|
async mintPlatformServiceToken(gameId, studentId) {
|
|
111230
|
-
|
|
111231
|
-
|
|
111232
|
-
|
|
111233
|
-
|
|
111234
|
-
aud: "playcademy-game-metrics",
|
|
111235
|
-
gameId,
|
|
111236
|
-
studentId,
|
|
111237
|
-
iat: now2,
|
|
111238
|
-
exp: now2 + 60,
|
|
111239
|
-
jti: crypto.randomUUID()
|
|
111240
|
-
}));
|
|
111241
|
-
return `${header}.${payload}.sandbox`;
|
|
111472
|
+
return mintSandboxServiceToken("playcademy-game-metrics", { gameId, studentId });
|
|
111473
|
+
},
|
|
111474
|
+
async mintSkillStatusToken(claims) {
|
|
111475
|
+
return mintSandboxServiceToken(SKILL_STATUS_TOKEN_AUDIENCE, { ...claims });
|
|
111242
111476
|
},
|
|
111243
111477
|
async mintAssessmentPreparationReceipt(plan) {
|
|
111244
111478
|
const now2 = Date.now();
|
|
@@ -111318,6 +111552,7 @@ function createSandboxAuthProvider() {
|
|
|
111318
111552
|
}
|
|
111319
111553
|
var sandboxApiKeys, sandboxAssessmentPreparationReceipts, ASSESSMENT_PREPARATION_RECEIPT_LIFETIME_MS, MAX_TOKEN_LIFETIME_SECONDS, sandboxAssessmentTokens;
|
|
111320
111554
|
var init_auth_provider = __esm(async () => {
|
|
111555
|
+
init_src();
|
|
111321
111556
|
init_src6();
|
|
111322
111557
|
init_constants();
|
|
111323
111558
|
await init_dist5();
|
|
@@ -167913,6 +168148,36 @@ var init_session_controller = __esm(() => {
|
|
|
167913
168148
|
});
|
|
167914
168149
|
});
|
|
167915
168150
|
|
|
168151
|
+
// ../api-core/src/controllers/skills.controller.ts
|
|
168152
|
+
async function getStatus4(ctx) {
|
|
168153
|
+
const requestingGameSlug = gameWorkerKeySlug(ctx.apiKey);
|
|
168154
|
+
if (!requestingGameSlug) {
|
|
168155
|
+
throw ApiError.forbidden("Skill status can only be requested by a deployed game worker");
|
|
168156
|
+
}
|
|
168157
|
+
const body2 = await parseRequestBody(ctx.request, SkillStatusRequestSchema);
|
|
168158
|
+
return ctx.services.skillStatus.getStatus({ ...body2, requestingGameSlug });
|
|
168159
|
+
}
|
|
168160
|
+
async function getStatusLocally(ctx) {
|
|
168161
|
+
await parseRequestBody(ctx.request, SkillStatusRequestSchema);
|
|
168162
|
+
return { supported: false, reason: "no_active_deployment" };
|
|
168163
|
+
}
|
|
168164
|
+
var SkillStatusRequestSchema, skills2;
|
|
168165
|
+
var init_skills_controller = __esm(() => {
|
|
168166
|
+
init_esm();
|
|
168167
|
+
init_src();
|
|
168168
|
+
init_errors2();
|
|
168169
|
+
init_utils7();
|
|
168170
|
+
SkillStatusRequestSchema = exports_external.object({
|
|
168171
|
+
game: exports_external.string().min(1),
|
|
168172
|
+
studentId: exports_external.string().min(1),
|
|
168173
|
+
skills: exports_external.array(exports_external.string()).min(1).max(SKILL_STATUS_MAX_SKILLS * 2)
|
|
168174
|
+
});
|
|
168175
|
+
skills2 = defineControllerNames("skills", {
|
|
168176
|
+
getStatus: requireAuth(getStatus4),
|
|
168177
|
+
getStatusLocally: requireAuth(getStatusLocally)
|
|
168178
|
+
});
|
|
168179
|
+
});
|
|
168180
|
+
|
|
167916
168181
|
// ../api-core/src/controllers/timeback.controller.ts
|
|
167917
168182
|
function parseQtiLibraryParams(searchParams) {
|
|
167918
168183
|
const requestedPage = Number(searchParams.get("page"));
|
|
@@ -168950,6 +169215,7 @@ var init_controllers = __esm(() => {
|
|
|
168950
169215
|
init_secrets_controller();
|
|
168951
169216
|
init_seed_controller();
|
|
168952
169217
|
init_session_controller();
|
|
169218
|
+
init_skills_controller();
|
|
168953
169219
|
init_timeback_controller();
|
|
168954
169220
|
init_upload_controller();
|
|
168955
169221
|
init_user_controller();
|
|
@@ -169763,6 +170029,16 @@ var init_leaderboard = __esm(async () => {
|
|
|
169763
170029
|
leaderboardRouter.get("/", handle2(leaderboard.getGlobalLeaderboard));
|
|
169764
170030
|
});
|
|
169765
170031
|
|
|
170032
|
+
// src/routes/platform/skills.ts
|
|
170033
|
+
var skillsRouter;
|
|
170034
|
+
var init_skills3 = __esm(async () => {
|
|
170035
|
+
init_dist7();
|
|
170036
|
+
init_controllers();
|
|
170037
|
+
await init_api3();
|
|
170038
|
+
skillsRouter = new Hono2;
|
|
170039
|
+
skillsRouter.post("/status", handle2(skills2.getStatusLocally));
|
|
170040
|
+
});
|
|
170041
|
+
|
|
169766
170042
|
// src/routes/integrations/timeback.ts
|
|
169767
170043
|
function hashCode(str) {
|
|
169768
170044
|
let hash2 = 0;
|
|
@@ -170111,6 +170387,7 @@ var exports_routes = {};
|
|
|
170111
170387
|
__export(exports_routes, {
|
|
170112
170388
|
usersRouter: () => usersRouter,
|
|
170113
170389
|
timebackRouter: () => timebackRouter,
|
|
170390
|
+
skillsRouter: () => skillsRouter,
|
|
170114
170391
|
manifestRouter: () => manifestRouter,
|
|
170115
170392
|
ltiRouter: () => ltiRouter,
|
|
170116
170393
|
leaderboardRouter: () => leaderboardRouter,
|
|
@@ -170127,6 +170404,7 @@ var init_routes = __esm(async () => {
|
|
|
170127
170404
|
init_users(),
|
|
170128
170405
|
init_games2(),
|
|
170129
170406
|
init_leaderboard(),
|
|
170407
|
+
init_skills3(),
|
|
170130
170408
|
init_timeback8(),
|
|
170131
170409
|
init_lti()
|
|
170132
170410
|
]);
|
|
@@ -170142,6 +170420,7 @@ async function registerRoutes(app) {
|
|
|
170142
170420
|
leaderboardRouter: leaderboardRouter2,
|
|
170143
170421
|
ltiRouter: ltiRouter2,
|
|
170144
170422
|
manifestRouter: manifestRouter2,
|
|
170423
|
+
skillsRouter: skillsRouter2,
|
|
170145
170424
|
timebackRouter: timebackRouter2,
|
|
170146
170425
|
usersRouter: usersRouter2
|
|
170147
170426
|
} = await init_routes().then(() => exports_routes);
|
|
@@ -170150,6 +170429,7 @@ async function registerRoutes(app) {
|
|
|
170150
170429
|
app.route("/api/users", usersRouter2);
|
|
170151
170430
|
app.route("/api/games", gamesRouter2);
|
|
170152
170431
|
app.route("/api/leaderboard", leaderboardRouter2);
|
|
170432
|
+
app.route("/api/skills", skillsRouter2);
|
|
170153
170433
|
app.route("/api/dev", devRouter2);
|
|
170154
170434
|
app.route("/api/timeback", timebackRouter2);
|
|
170155
170435
|
app.route("/api/lti", ltiRouter2);
|