@playcademy/sandbox 0.10.1 → 0.10.2-beta.2
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 +385 -106
- package/dist/constants.js +14 -0
- package/dist/server.js +385 -106
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -281,6 +281,19 @@ var init_platform = __esm(() => {
|
|
|
281
281
|
};
|
|
282
282
|
});
|
|
283
283
|
|
|
284
|
+
// ../constants/src/skills.ts
|
|
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
|
+
});
|
|
296
|
+
|
|
284
297
|
// ../constants/src/system.ts
|
|
285
298
|
var PLATFORM_TIMEZONE = "America/New_York";
|
|
286
299
|
|
|
@@ -373,6 +386,7 @@ var init_src = __esm(() => {
|
|
|
373
386
|
init_env_vars();
|
|
374
387
|
init_game();
|
|
375
388
|
init_platform();
|
|
389
|
+
init_skills();
|
|
376
390
|
init_timeback2();
|
|
377
391
|
init_cloudflare();
|
|
378
392
|
});
|
|
@@ -1128,7 +1142,7 @@ var package_default;
|
|
|
1128
1142
|
var init_package = __esm(() => {
|
|
1129
1143
|
package_default = {
|
|
1130
1144
|
name: "@playcademy/sandbox",
|
|
1131
|
-
version: "0.10.
|
|
1145
|
+
version: "0.10.2-beta.2",
|
|
1132
1146
|
description: "Local development server for Playcademy game development",
|
|
1133
1147
|
type: "module",
|
|
1134
1148
|
exports: {
|
|
@@ -30005,6 +30019,23 @@ function generateDeploymentHash(code) {
|
|
|
30005
30019
|
function computeDeployPayloadFingerprint(payload) {
|
|
30006
30020
|
return sha256Hex(canonicalJson2(payload));
|
|
30007
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
|
+
}
|
|
30008
30039
|
var GAME_WORKER_KEY_PREFIX = "game-worker-", DASHBOARD_WORKER_KEY_PREFIX = "dash-worker-";
|
|
30009
30040
|
var init_deployment_util = __esm(() => {
|
|
30010
30041
|
init_drizzle_orm();
|
|
@@ -37028,24 +37059,9 @@ class DeployService {
|
|
|
37028
37059
|
}
|
|
37029
37060
|
}
|
|
37030
37061
|
async saveDeployment(record) {
|
|
37031
|
-
|
|
37032
|
-
await db2.transaction(async (tx) => {
|
|
37062
|
+
await this.deps.db.transaction(async (tx) => {
|
|
37033
37063
|
await tx.update(gameDeployments).set({ isActive: false }).where(and(eq(gameDeployments.gameId, record.gameId), eq(gameDeployments.target, record.target)));
|
|
37034
|
-
await tx.insert(gameDeployments).values(
|
|
37035
|
-
gameId: record.gameId,
|
|
37036
|
-
deploymentId: record.deploymentId,
|
|
37037
|
-
provider: "cloudflare",
|
|
37038
|
-
target: record.target,
|
|
37039
|
-
url: record.url,
|
|
37040
|
-
codeHash: record.codeHash,
|
|
37041
|
-
schemaHash: record.schemaHash ?? null,
|
|
37042
|
-
schemaFingerprint: record.schemaFingerprint ?? null,
|
|
37043
|
-
timeTravelBookmark: record.timeTravelBookmark ?? null,
|
|
37044
|
-
bookmarkCapturedAt: record.bookmarkCapturedAt ?? null,
|
|
37045
|
-
resources: record.resources,
|
|
37046
|
-
isActive: true,
|
|
37047
|
-
deployJobId: record.deployJobId ?? null
|
|
37048
|
-
});
|
|
37064
|
+
await tx.insert(gameDeployments).values(toActiveDeploymentRow(record));
|
|
37049
37065
|
});
|
|
37050
37066
|
}
|
|
37051
37067
|
async notifyDeploymentFailure(failure) {
|
|
@@ -46877,6 +46893,39 @@ function publishableRepositoryUrl(value) {
|
|
|
46877
46893
|
return value;
|
|
46878
46894
|
}
|
|
46879
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
|
+
|
|
46880
46929
|
// ../api-core/src/services/releases.service.ts
|
|
46881
46930
|
class ReleasesService {
|
|
46882
46931
|
deps;
|
|
@@ -46901,9 +46950,7 @@ class ReleasesService {
|
|
|
46901
46950
|
"app.releases.history_requested": history ?? 0,
|
|
46902
46951
|
"app.releases.history_rows": rows.length
|
|
46903
46952
|
});
|
|
46904
|
-
|
|
46905
|
-
const current = releases[0] ?? null;
|
|
46906
|
-
return history === undefined ? { current } : { current, history: releases };
|
|
46953
|
+
return toGameReleaseResponse(rows, history);
|
|
46907
46954
|
}
|
|
46908
46955
|
}
|
|
46909
46956
|
function releaseColumns() {
|
|
@@ -46920,36 +46967,12 @@ function listedGame() {
|
|
|
46920
46967
|
function activeWorkerDeployment() {
|
|
46921
46968
|
return and(eq(gameDeployments.isActive, true), eq(gameDeployments.target, "game"));
|
|
46922
46969
|
}
|
|
46923
|
-
function toRelease(row) {
|
|
46924
|
-
return {
|
|
46925
|
-
slug: row.slug,
|
|
46926
|
-
url: row.url,
|
|
46927
|
-
source: normalizeSource(row.source),
|
|
46928
|
-
deployedAt: row.deployedAt.toISOString()
|
|
46929
|
-
};
|
|
46930
|
-
}
|
|
46931
|
-
function normalizeSource(value) {
|
|
46932
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
46933
|
-
return null;
|
|
46934
|
-
}
|
|
46935
|
-
const raw = value;
|
|
46936
|
-
const repository = nullableString(raw.repository);
|
|
46937
|
-
return {
|
|
46938
|
-
repository: repository === null ? null : publishableRepositoryUrl(repository),
|
|
46939
|
-
commit: nullableString(raw.commit),
|
|
46940
|
-
branch: nullableString(raw.branch),
|
|
46941
|
-
dirty: raw.dirty === true,
|
|
46942
|
-
externalBuild: raw.externalBuild === true
|
|
46943
|
-
};
|
|
46944
|
-
}
|
|
46945
|
-
function nullableString(value) {
|
|
46946
|
-
return typeof value === "string" && value.length > 0 ? value : null;
|
|
46947
|
-
}
|
|
46948
46970
|
var init_releases_service = __esm(() => {
|
|
46949
46971
|
init_drizzle_orm();
|
|
46950
46972
|
init_tables_index();
|
|
46951
46973
|
init_spans();
|
|
46952
46974
|
init_errors2();
|
|
46975
|
+
init_releases_util();
|
|
46953
46976
|
});
|
|
46954
46977
|
|
|
46955
46978
|
// ../api-core/src/services/secrets.service.ts
|
|
@@ -47405,6 +47428,226 @@ var init_seed_service = __esm(() => {
|
|
|
47405
47428
|
init_deployment_util();
|
|
47406
47429
|
});
|
|
47407
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
|
+
|
|
47408
47651
|
// ../../node_modules/.bun/drizzle-zod@0.7.1+3ad0422b77d1a05b/node_modules/drizzle-zod/index.mjs
|
|
47409
47652
|
function isColumnType(column2, columnTypes) {
|
|
47410
47653
|
return columnTypes.includes(column2.columnType);
|
|
@@ -93714,15 +93957,10 @@ class RosterStudents {
|
|
|
93714
93957
|
if (isApiError(error88) && error88.statusCode === 401) {
|
|
93715
93958
|
throw new TimebackAuthenticationError;
|
|
93716
93959
|
}
|
|
93717
|
-
if (error88 instanceof
|
|
93718
|
-
|
|
93960
|
+
if (!(error88 instanceof TimebackError)) {
|
|
93961
|
+
logTimebackError("student email lookup", error88, { student_email: email5 });
|
|
93719
93962
|
}
|
|
93720
|
-
|
|
93721
|
-
"app.timeback.student_email": email5,
|
|
93722
|
-
"exception.type": errorType(error88),
|
|
93723
|
-
"app.error.message": errorMessage(error88)
|
|
93724
|
-
});
|
|
93725
|
-
throw new StudentNotFoundError(email5, "email");
|
|
93963
|
+
throw error88;
|
|
93726
93964
|
}
|
|
93727
93965
|
}
|
|
93728
93966
|
async resolveById(studentId, providedEmail) {
|
|
@@ -98259,12 +98497,6 @@ class TimebackAdminService {
|
|
|
98259
98497
|
grade: integration.grade
|
|
98260
98498
|
};
|
|
98261
98499
|
}
|
|
98262
|
-
resolveGameMetricsBaseUrl(slug2, deployedUrl) {
|
|
98263
|
-
if (!slug2) {
|
|
98264
|
-
return deployedUrl;
|
|
98265
|
-
}
|
|
98266
|
-
return this.deps.config.localGameUrls[slug2] ?? deployedUrl;
|
|
98267
|
-
}
|
|
98268
98500
|
static resolveGameMetricsUrl(baseUrl, runIds) {
|
|
98269
98501
|
try {
|
|
98270
98502
|
const url4 = new URL("/__playcademy/metrics", baseUrl);
|
|
@@ -98298,9 +98530,6 @@ class TimebackAdminService {
|
|
|
98298
98530
|
}
|
|
98299
98531
|
return chunks;
|
|
98300
98532
|
}
|
|
98301
|
-
static isAbortError(error88) {
|
|
98302
|
-
return error88 instanceof Error && error88.name === "AbortError";
|
|
98303
|
-
}
|
|
98304
98533
|
static roundXpToTenths(value) {
|
|
98305
98534
|
const rounded = Math.round(value * TimebackAdminService.XP_PRECISION_FACTOR) / TimebackAdminService.XP_PRECISION_FACTOR;
|
|
98306
98535
|
return Object.is(rounded, -0) ? 0 : rounded;
|
|
@@ -98938,7 +99167,7 @@ class TimebackAdminService {
|
|
|
98938
99167
|
if (enrollmentsByCourseId.size === 0) {
|
|
98939
99168
|
throw new NotFoundError("Student enrollment", `${timebackId}:${gameId}`);
|
|
98940
99169
|
}
|
|
98941
|
-
const metricsBaseUrl = this.
|
|
99170
|
+
const metricsBaseUrl = resolveGameWorkerBaseUrl(this.deps.config.localGameUrls, game2?.slug, deployment?.url);
|
|
98942
99171
|
if (!metricsBaseUrl) {
|
|
98943
99172
|
setAttributes({
|
|
98944
99173
|
"app.timeback.metrics.supported": false,
|
|
@@ -98961,40 +99190,18 @@ class TimebackAdminService {
|
|
|
98961
99190
|
};
|
|
98962
99191
|
}
|
|
98963
99192
|
const token = await this.deps.mintPlatformServiceToken(gameId, targetUser.id);
|
|
98964
|
-
const
|
|
98965
|
-
|
|
98966
|
-
|
|
98967
|
-
|
|
98968
|
-
|
|
98969
|
-
method: "GET",
|
|
98970
|
-
headers: {
|
|
98971
|
-
Accept: "application/json",
|
|
98972
|
-
Authorization: `Bearer ${token}`
|
|
98973
|
-
},
|
|
98974
|
-
signal: controller.signal
|
|
98975
|
-
});
|
|
98976
|
-
} catch (error88) {
|
|
98977
|
-
const timedOut = TimebackAdminService.isAbortError(error88);
|
|
98978
|
-
let details;
|
|
98979
|
-
if (timedOut) {
|
|
98980
|
-
details = "Game metrics request timed out";
|
|
98981
|
-
} else if (error88 instanceof Error) {
|
|
98982
|
-
details = error88.message;
|
|
98983
|
-
} else {
|
|
98984
|
-
details = String(error88);
|
|
98985
|
-
}
|
|
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) {
|
|
98986
99198
|
setAttributes({
|
|
98987
99199
|
"app.timeback.metrics.supported": false,
|
|
98988
|
-
"app.timeback.metrics.reason":
|
|
99200
|
+
"app.timeback.metrics.reason": call.reason
|
|
98989
99201
|
});
|
|
98990
|
-
return {
|
|
98991
|
-
supported: false,
|
|
98992
|
-
reason: timedOut ? "timeout" : "fetch_failed",
|
|
98993
|
-
details
|
|
98994
|
-
};
|
|
98995
|
-
} finally {
|
|
98996
|
-
clearTimeout(timeout);
|
|
99202
|
+
return { supported: false, reason: call.reason, details: call.details };
|
|
98997
99203
|
}
|
|
99204
|
+
const { response } = call;
|
|
98998
99205
|
const result = await classifyGameMetricsResponse(response, { gameId, timebackId });
|
|
98999
99206
|
setAttributes({
|
|
99000
99207
|
"app.timeback.metrics.supported": result.supported,
|
|
@@ -99992,6 +100199,7 @@ var init_timeback_admin_service = __esm(async () => {
|
|
|
99992
100199
|
init_src9();
|
|
99993
100200
|
init_timeback3();
|
|
99994
100201
|
init_errors2();
|
|
100202
|
+
init_game_worker_call_util();
|
|
99995
100203
|
init_timeback_admin_metrics_util();
|
|
99996
100204
|
init_timeback_admin_util();
|
|
99997
100205
|
init_timeback_discrepancy_queue_util();
|
|
@@ -109903,6 +110111,7 @@ function createPlatformServices(deps) {
|
|
|
109903
110111
|
timebackClient,
|
|
109904
110112
|
alerts,
|
|
109905
110113
|
mintPlatformServiceToken,
|
|
110114
|
+
mintSkillStatusToken,
|
|
109906
110115
|
mintAssessmentPreparationReceipt,
|
|
109907
110116
|
verifyAssessmentPreparationReceipt,
|
|
109908
110117
|
mintAssessmentToken,
|
|
@@ -109986,6 +110195,11 @@ function createPlatformServices(deps) {
|
|
|
109986
110195
|
mintAssessmentToken,
|
|
109987
110196
|
verifyAssessmentToken
|
|
109988
110197
|
});
|
|
110198
|
+
const skillStatus = new SkillStatusService({
|
|
110199
|
+
db: db2,
|
|
110200
|
+
localGameUrls: config5.localGameUrls,
|
|
110201
|
+
mintSkillStatusToken
|
|
110202
|
+
});
|
|
109989
110203
|
return {
|
|
109990
110204
|
assessmentAssets,
|
|
109991
110205
|
assessmentArtifacts,
|
|
@@ -109998,6 +110212,7 @@ function createPlatformServices(deps) {
|
|
|
109998
110212
|
deploymentState,
|
|
109999
110213
|
database,
|
|
110000
110214
|
seed,
|
|
110215
|
+
skillStatus,
|
|
110001
110216
|
timeback: timeback2,
|
|
110002
110217
|
timebackAdmin,
|
|
110003
110218
|
timebackAssessmentRuntime,
|
|
@@ -110016,6 +110231,7 @@ var init_platform2 = __esm(async () => {
|
|
|
110016
110231
|
init_releases_service();
|
|
110017
110232
|
init_secrets_service();
|
|
110018
110233
|
init_seed_service();
|
|
110234
|
+
init_skill_status_service();
|
|
110019
110235
|
init_upload_service();
|
|
110020
110236
|
await __promiseAll([
|
|
110021
110237
|
init_timeback_admin_service(),
|
|
@@ -110734,6 +110950,7 @@ function createServices(ctx) {
|
|
|
110734
110950
|
timebackClient: timeback2,
|
|
110735
110951
|
alerts: infra2.alerts,
|
|
110736
110952
|
mintPlatformServiceToken: auth2.mintPlatformServiceToken.bind(auth2),
|
|
110953
|
+
mintSkillStatusToken: auth2.mintSkillStatusToken.bind(auth2),
|
|
110737
110954
|
mintAssessmentPreparationReceipt: auth2.mintAssessmentPreparationReceipt.bind(auth2),
|
|
110738
110955
|
verifyAssessmentPreparationReceipt: auth2.verifyAssessmentPreparationReceipt.bind(auth2),
|
|
110739
110956
|
mintAssessmentToken: auth2.mintAssessmentToken.bind(auth2),
|
|
@@ -110782,6 +110999,17 @@ function workerKeyGrantsGameRead(key, slug2) {
|
|
|
110782
110999
|
function isGameWorkerKey(key) {
|
|
110783
111000
|
return Boolean(key?.name?.startsWith(GAME_WORKER_KEY_PREFIX));
|
|
110784
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
|
+
}
|
|
110785
111013
|
function deniedWorkerKeyRequest(key, method, pathname) {
|
|
110786
111014
|
if (isDashboardWorkerKey(key) && !isAllowedDashboardWorkerRequest(method, pathname, key.permissions)) {
|
|
110787
111015
|
return DASHBOARD_WORKER_KEY_RESTRICTED;
|
|
@@ -111158,6 +111386,19 @@ var init_clients = __esm(async () => {
|
|
|
111158
111386
|
});
|
|
111159
111387
|
|
|
111160
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
|
+
}
|
|
111161
111402
|
function createSandboxAuthProvider() {
|
|
111162
111403
|
return {
|
|
111163
111404
|
async createApiKey(params) {
|
|
@@ -111229,18 +111470,10 @@ function createSandboxAuthProvider() {
|
|
|
111229
111470
|
}
|
|
111230
111471
|
},
|
|
111231
111472
|
async mintPlatformServiceToken(gameId, studentId) {
|
|
111232
|
-
|
|
111233
|
-
|
|
111234
|
-
|
|
111235
|
-
|
|
111236
|
-
aud: "playcademy-game-metrics",
|
|
111237
|
-
gameId,
|
|
111238
|
-
studentId,
|
|
111239
|
-
iat: now2,
|
|
111240
|
-
exp: now2 + 60,
|
|
111241
|
-
jti: crypto.randomUUID()
|
|
111242
|
-
}));
|
|
111243
|
-
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 });
|
|
111244
111477
|
},
|
|
111245
111478
|
async mintAssessmentPreparationReceipt(plan) {
|
|
111246
111479
|
const now2 = Date.now();
|
|
@@ -111320,6 +111553,7 @@ function createSandboxAuthProvider() {
|
|
|
111320
111553
|
}
|
|
111321
111554
|
var sandboxApiKeys, sandboxAssessmentPreparationReceipts, ASSESSMENT_PREPARATION_RECEIPT_LIFETIME_MS, MAX_TOKEN_LIFETIME_SECONDS, sandboxAssessmentTokens;
|
|
111322
111555
|
var init_auth_provider = __esm(async () => {
|
|
111556
|
+
init_src();
|
|
111323
111557
|
init_src6();
|
|
111324
111558
|
init_constants();
|
|
111325
111559
|
await init_dist5();
|
|
@@ -167915,6 +168149,36 @@ var init_session_controller = __esm(() => {
|
|
|
167915
168149
|
});
|
|
167916
168150
|
});
|
|
167917
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
|
+
|
|
167918
168182
|
// ../api-core/src/controllers/timeback.controller.ts
|
|
167919
168183
|
function parseQtiLibraryParams(searchParams) {
|
|
167920
168184
|
const requestedPage = Number(searchParams.get("page"));
|
|
@@ -168952,6 +169216,7 @@ var init_controllers = __esm(() => {
|
|
|
168952
169216
|
init_secrets_controller();
|
|
168953
169217
|
init_seed_controller();
|
|
168954
169218
|
init_session_controller();
|
|
169219
|
+
init_skills_controller();
|
|
168955
169220
|
init_timeback_controller();
|
|
168956
169221
|
init_upload_controller();
|
|
168957
169222
|
init_user_controller();
|
|
@@ -169765,6 +170030,16 @@ var init_leaderboard = __esm(async () => {
|
|
|
169765
170030
|
leaderboardRouter.get("/", handle2(leaderboard.getGlobalLeaderboard));
|
|
169766
170031
|
});
|
|
169767
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
|
+
|
|
169768
170043
|
// src/routes/integrations/timeback.ts
|
|
169769
170044
|
function hashCode(str) {
|
|
169770
170045
|
let hash2 = 0;
|
|
@@ -170113,6 +170388,7 @@ var exports_routes = {};
|
|
|
170113
170388
|
__export(exports_routes, {
|
|
170114
170389
|
usersRouter: () => usersRouter,
|
|
170115
170390
|
timebackRouter: () => timebackRouter,
|
|
170391
|
+
skillsRouter: () => skillsRouter,
|
|
170116
170392
|
manifestRouter: () => manifestRouter,
|
|
170117
170393
|
ltiRouter: () => ltiRouter,
|
|
170118
170394
|
leaderboardRouter: () => leaderboardRouter,
|
|
@@ -170129,6 +170405,7 @@ var init_routes = __esm(async () => {
|
|
|
170129
170405
|
init_users(),
|
|
170130
170406
|
init_games2(),
|
|
170131
170407
|
init_leaderboard(),
|
|
170408
|
+
init_skills3(),
|
|
170132
170409
|
init_timeback8(),
|
|
170133
170410
|
init_lti()
|
|
170134
170411
|
]);
|
|
@@ -170144,6 +170421,7 @@ async function registerRoutes(app) {
|
|
|
170144
170421
|
leaderboardRouter: leaderboardRouter2,
|
|
170145
170422
|
ltiRouter: ltiRouter2,
|
|
170146
170423
|
manifestRouter: manifestRouter2,
|
|
170424
|
+
skillsRouter: skillsRouter2,
|
|
170147
170425
|
timebackRouter: timebackRouter2,
|
|
170148
170426
|
usersRouter: usersRouter2
|
|
170149
170427
|
} = await init_routes().then(() => exports_routes);
|
|
@@ -170152,6 +170430,7 @@ async function registerRoutes(app) {
|
|
|
170152
170430
|
app.route("/api/users", usersRouter2);
|
|
170153
170431
|
app.route("/api/games", gamesRouter2);
|
|
170154
170432
|
app.route("/api/leaderboard", leaderboardRouter2);
|
|
170433
|
+
app.route("/api/skills", skillsRouter2);
|
|
170155
170434
|
app.route("/api/dev", devRouter2);
|
|
170156
170435
|
app.route("/api/timeback", timebackRouter2);
|
|
170157
170436
|
app.route("/api/lti", ltiRouter2);
|