playcademy 0.14.18 → 0.14.19-alpha.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/README.md +20 -0
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -2
- package/dist/db.js +77 -27
- package/dist/edge-play/src/register-routes.ts +7 -0
- package/dist/edge-play/src/routes/health.ts +18 -7
- package/dist/edge-play/src/routes/integrations/timeback/end-activity.ts +113 -49
- package/dist/edge-play/src/types.ts +2 -0
- package/dist/index.d.ts +79 -19
- package/dist/index.js +7141 -7116
- package/dist/templates/api/sample-route-with-db.ts.template +1 -1
- package/dist/templates/api/sample-route.ts.template +5 -5
- package/dist/templates/auth/auth-schema.ts.template +1 -1
- package/dist/templates/auth/auth.ts.template +2 -2
- package/dist/templates/database/db-schema-example.ts.template +1 -1
- package/dist/utils.d.ts +84 -11
- package/dist/utils.js +257 -136
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -213,6 +213,26 @@ Credentials are stored in `~/.playcademy/auth.json`:
|
|
|
213
213
|
|
|
214
214
|
Set up TimeBack LTI integration for your game.
|
|
215
215
|
|
|
216
|
+
**Note:** Before running this command, ensure that `totalXp` is configured for each course in your `playcademy.config.js`:
|
|
217
|
+
|
|
218
|
+
```js
|
|
219
|
+
integrations: {
|
|
220
|
+
timeback: {
|
|
221
|
+
courses: [
|
|
222
|
+
{
|
|
223
|
+
subject: 'Math',
|
|
224
|
+
grade: 3,
|
|
225
|
+
metadata: {
|
|
226
|
+
metrics: {
|
|
227
|
+
totalXp: 1000, // Required
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
]
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
216
236
|
```bash
|
|
217
237
|
playcademy timeback setup
|
|
218
238
|
playcademy timeback setup --dry-run
|
package/dist/constants.d.ts
CHANGED
|
@@ -278,7 +278,7 @@ declare const CLI_FILES: {
|
|
|
278
278
|
declare const DEFAULT_PORTS: {
|
|
279
279
|
/** Sandbox server (mock platform API) */
|
|
280
280
|
readonly SANDBOX: 4321;
|
|
281
|
-
/** Backend dev server (
|
|
281
|
+
/** Backend dev server (project backend with HMR) */
|
|
282
282
|
readonly BACKEND: 8788;
|
|
283
283
|
};
|
|
284
284
|
|
package/dist/constants.js
CHANGED
|
@@ -31,7 +31,6 @@ var package_default = {
|
|
|
31
31
|
sharp: "^0.34.2",
|
|
32
32
|
typedoc: "^0.28.5",
|
|
33
33
|
"typedoc-plugin-markdown": "^4.7.0",
|
|
34
|
-
"typedoc-vitepress-theme": "^1.1.2",
|
|
35
34
|
"typescript-eslint": "^8.30.1",
|
|
36
35
|
"yocto-spinner": "^0.2.2"
|
|
37
36
|
},
|
|
@@ -255,7 +254,7 @@ var CLI_FILES = {
|
|
|
255
254
|
var DEFAULT_PORTS = {
|
|
256
255
|
/** Sandbox server (mock platform API) */
|
|
257
256
|
SANDBOX: 4321,
|
|
258
|
-
/** Backend dev server (
|
|
257
|
+
/** Backend dev server (project backend with HMR) */
|
|
259
258
|
BACKEND: 8788
|
|
260
259
|
};
|
|
261
260
|
|
package/dist/db.js
CHANGED
|
@@ -1458,7 +1458,6 @@ var package_default = {
|
|
|
1458
1458
|
sharp: "^0.34.2",
|
|
1459
1459
|
typedoc: "^0.28.5",
|
|
1460
1460
|
"typedoc-plugin-markdown": "^4.7.0",
|
|
1461
|
-
"typedoc-vitepress-theme": "^1.1.2",
|
|
1462
1461
|
"typescript-eslint": "^8.30.1",
|
|
1463
1462
|
"yocto-spinner": "^0.2.2"
|
|
1464
1463
|
},
|
|
@@ -2017,7 +2016,7 @@ function getRunCommand(pm, script) {
|
|
|
2017
2016
|
}
|
|
2018
2017
|
|
|
2019
2018
|
// src/lib/core/client.ts
|
|
2020
|
-
import { PlaycademyClient } from "@playcademy/sdk";
|
|
2019
|
+
import { PlaycademyClient } from "@playcademy/sdk/internal";
|
|
2021
2020
|
|
|
2022
2021
|
// src/lib/core/context.ts
|
|
2023
2022
|
var context = {};
|
|
@@ -2043,7 +2042,7 @@ import {
|
|
|
2043
2042
|
gray,
|
|
2044
2043
|
green,
|
|
2045
2044
|
greenBright,
|
|
2046
|
-
red
|
|
2045
|
+
red,
|
|
2047
2046
|
yellow,
|
|
2048
2047
|
yellowBright
|
|
2049
2048
|
} from "colorette";
|
|
@@ -2636,34 +2635,77 @@ var eraseLine = ESC + "2K";
|
|
|
2636
2635
|
import colors3 from "yoctocolors-cjs";
|
|
2637
2636
|
|
|
2638
2637
|
// src/lib/core/error.ts
|
|
2639
|
-
import { bold as bold2, dim as dim2,
|
|
2640
|
-
import { ApiError, extractApiErrorInfo } from "@playcademy/sdk";
|
|
2638
|
+
import { bold as bold2, dim as dim2, redBright } from "colorette";
|
|
2639
|
+
import { ApiError, extractApiErrorInfo } from "@playcademy/sdk/internal";
|
|
2641
2640
|
function isConfigError(error) {
|
|
2642
2641
|
return error !== null && typeof error === "object" && "name" in error && error.name === "ConfigError" && "message" in error;
|
|
2643
2642
|
}
|
|
2643
|
+
function extractEmbeddedJson(message) {
|
|
2644
|
+
const jsonMatch = message.match(/(\{.+\})$/);
|
|
2645
|
+
if (!jsonMatch) {
|
|
2646
|
+
return { cleanMessage: message };
|
|
2647
|
+
}
|
|
2648
|
+
try {
|
|
2649
|
+
const json = JSON.parse(jsonMatch[1]);
|
|
2650
|
+
const cleanMessage = message.slice(0, jsonMatch.index).trim();
|
|
2651
|
+
return { cleanMessage, json };
|
|
2652
|
+
} catch {
|
|
2653
|
+
return { cleanMessage: message };
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
function cleanMessageSuffix(message) {
|
|
2657
|
+
let cleaned = message.replace(/:\s*\d{3}\s*$/, "").trim();
|
|
2658
|
+
if (cleaned.endsWith(":")) {
|
|
2659
|
+
cleaned = cleaned.slice(0, -1).trim();
|
|
2660
|
+
}
|
|
2661
|
+
return cleaned;
|
|
2662
|
+
}
|
|
2663
|
+
function removeStatusPrefix(message, statusCode) {
|
|
2664
|
+
if (message.startsWith(`${statusCode} `)) {
|
|
2665
|
+
return message.slice(`${statusCode} `.length);
|
|
2666
|
+
}
|
|
2667
|
+
return message;
|
|
2668
|
+
}
|
|
2644
2669
|
function displayApiError(error, indent) {
|
|
2645
2670
|
const spaces = " ".repeat(indent);
|
|
2646
2671
|
const errorInfo = extractApiErrorInfo(error);
|
|
2647
|
-
if (errorInfo) {
|
|
2648
|
-
console.error(`${spaces}${
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
}
|
|
2653
|
-
|
|
2672
|
+
if (!errorInfo) {
|
|
2673
|
+
console.error(`${spaces}${redBright("\u2716")} ${bold2(error.message)}`);
|
|
2674
|
+
if (process.env.DEBUG && error.details) {
|
|
2675
|
+
console.error("");
|
|
2676
|
+
logger.json(error.details, indent + 1);
|
|
2677
|
+
}
|
|
2678
|
+
return;
|
|
2679
|
+
}
|
|
2680
|
+
const statusCode = errorInfo.status;
|
|
2681
|
+
let displayMessage = errorInfo.statusText;
|
|
2682
|
+
displayMessage = removeStatusPrefix(displayMessage, statusCode);
|
|
2683
|
+
const { cleanMessage, json: embeddedJson } = extractEmbeddedJson(displayMessage);
|
|
2684
|
+
displayMessage = cleanMessageSuffix(cleanMessage);
|
|
2685
|
+
let errorCode;
|
|
2686
|
+
if (error.details && typeof error.details === "object") {
|
|
2687
|
+
const details = error.details;
|
|
2688
|
+
if ("code" in details && typeof details.code === "string") {
|
|
2689
|
+
errorCode = details.code;
|
|
2654
2690
|
}
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2691
|
+
}
|
|
2692
|
+
let errorHeader = "API Error";
|
|
2693
|
+
if (errorCode) {
|
|
2694
|
+
errorHeader += ` ${redBright(`[${errorCode}]`)}`;
|
|
2695
|
+
}
|
|
2696
|
+
errorHeader += `: ${displayMessage} ${redBright(`[${statusCode}]`)}`;
|
|
2697
|
+
console.error(`${spaces}${redBright("\u2716")} ${bold2(errorHeader)}`);
|
|
2698
|
+
if (process.env.DEBUG) {
|
|
2699
|
+
const detailsToShow = embeddedJson || error.details || errorInfo.details;
|
|
2700
|
+
if (detailsToShow) {
|
|
2701
|
+
console.error("");
|
|
2702
|
+
logger.json(detailsToShow, indent + 1);
|
|
2659
2703
|
}
|
|
2660
|
-
} else {
|
|
2661
|
-
console.error(`${spaces}${red("\u2716")} ${bold2(error.message)}`);
|
|
2662
2704
|
}
|
|
2663
2705
|
}
|
|
2664
2706
|
function displayConfigError(error, indent) {
|
|
2665
2707
|
const spaces = " ".repeat(indent);
|
|
2666
|
-
console.error(`${spaces}${
|
|
2708
|
+
console.error(`${spaces}${redBright("\u2716")} ${bold2(error.message)}`);
|
|
2667
2709
|
if (error.field) {
|
|
2668
2710
|
console.error(`${spaces} ${dim2("Field:")} ${error.field}`);
|
|
2669
2711
|
}
|
|
@@ -2673,7 +2715,7 @@ function displayConfigError(error, indent) {
|
|
|
2673
2715
|
}
|
|
2674
2716
|
function displayGenericError(error, indent) {
|
|
2675
2717
|
const spaces = " ".repeat(indent);
|
|
2676
|
-
console.error(`${spaces}${
|
|
2718
|
+
console.error(`${spaces}${redBright("\u2716")} ${bold2(error.message)}`);
|
|
2677
2719
|
if (error.stack && process.env.DEBUG) {
|
|
2678
2720
|
console.error(`${spaces} ${dim2("Stack:")}`);
|
|
2679
2721
|
console.error(
|
|
@@ -2695,7 +2737,7 @@ function formatError(error, indent = 0) {
|
|
|
2695
2737
|
displayGenericError(error, indent);
|
|
2696
2738
|
return;
|
|
2697
2739
|
}
|
|
2698
|
-
console.error(`${spaces}${
|
|
2740
|
+
console.error(`${spaces}${redBright("\u2716")} ${bold2(String(error))}`);
|
|
2699
2741
|
}
|
|
2700
2742
|
|
|
2701
2743
|
// src/lib/core/logger.ts
|
|
@@ -2706,12 +2748,20 @@ function customTransform(text) {
|
|
|
2706
2748
|
return result;
|
|
2707
2749
|
}
|
|
2708
2750
|
function formatTable(data, title) {
|
|
2751
|
+
const ANSI_REGEX = /\u001B\[[0-9;]*m/g;
|
|
2752
|
+
const stripAnsi2 = (value) => value.replace(ANSI_REGEX, "");
|
|
2753
|
+
const visibleLength = (value) => stripAnsi2(value).length;
|
|
2754
|
+
const padCell = (value, width) => {
|
|
2755
|
+
const length = visibleLength(value);
|
|
2756
|
+
if (length >= width) return value;
|
|
2757
|
+
return value + " ".repeat(width - length);
|
|
2758
|
+
};
|
|
2709
2759
|
if (data.length === 0) return;
|
|
2710
2760
|
const keys = Object.keys(data[0]);
|
|
2711
2761
|
const rows = data.map((item) => keys.map((key) => String(item[key] ?? "")));
|
|
2712
2762
|
const widths = keys.map((key, i) => {
|
|
2713
|
-
const headerWidth = key
|
|
2714
|
-
const dataWidth = Math.max(...rows.map((row) => row[i]
|
|
2763
|
+
const headerWidth = visibleLength(key);
|
|
2764
|
+
const dataWidth = Math.max(...rows.map((row) => visibleLength(row[i])));
|
|
2715
2765
|
return Math.max(headerWidth, dataWidth);
|
|
2716
2766
|
});
|
|
2717
2767
|
const totalWidth = widths.reduce((sum, w) => sum + w + 3, -1);
|
|
@@ -2727,11 +2777,11 @@ function formatTable(data, title) {
|
|
|
2727
2777
|
console.log(titleRow);
|
|
2728
2778
|
console.log(titleSeparator);
|
|
2729
2779
|
}
|
|
2730
|
-
const header = "\u2502 " + keys.map((key, i) => key
|
|
2780
|
+
const header = "\u2502 " + keys.map((key, i) => padCell(key, widths[i])).join(" \u2502 ") + " \u2502";
|
|
2731
2781
|
console.log(header);
|
|
2732
2782
|
console.log(separator);
|
|
2733
2783
|
rows.forEach((row) => {
|
|
2734
|
-
const dataRow = "\u2502 " + row.map((cell, i) => cell
|
|
2784
|
+
const dataRow = "\u2502 " + row.map((cell, i) => padCell(cell, widths[i])).join(" \u2502 ") + " \u2502";
|
|
2735
2785
|
console.log(dataRow);
|
|
2736
2786
|
});
|
|
2737
2787
|
console.log(bottomBorder);
|
|
@@ -2790,7 +2840,7 @@ var logger = {
|
|
|
2790
2840
|
*/
|
|
2791
2841
|
error: (message, indent = 0) => {
|
|
2792
2842
|
const spaces = " ".repeat(indent);
|
|
2793
|
-
console.error(`${spaces}${
|
|
2843
|
+
console.error(`${spaces}${red("\u2716")} ${customTransform(message)}`);
|
|
2794
2844
|
},
|
|
2795
2845
|
bold: (message, indent = 0) => {
|
|
2796
2846
|
const spaces = " ".repeat(indent);
|
|
@@ -2858,7 +2908,7 @@ var logger = {
|
|
|
2858
2908
|
const oldSize = formatSize(previousSize);
|
|
2859
2909
|
const newSize = formatSize(currentSize);
|
|
2860
2910
|
const delta = dim3(`(${formatDelta(currentSize - previousSize)})`);
|
|
2861
|
-
const value = `${
|
|
2911
|
+
const value = `${red(oldSize)} \u2192 ${green(newSize)} ${delta}`;
|
|
2862
2912
|
console.log(`${spaces}${dim3(label + ":")} ${bold3(value)}`);
|
|
2863
2913
|
},
|
|
2864
2914
|
/**
|
|
@@ -33,6 +33,13 @@ export async function registerBuiltinRoutes(app: Hono<HonoEnv>, integrations?: I
|
|
|
33
33
|
])
|
|
34
34
|
app.post(ROUTES.TIMEBACK.END_ACTIVITY, endActivity.POST)
|
|
35
35
|
// ... other routes
|
|
36
|
+
} else if (integrations?.timeback === null) {
|
|
37
|
+
app.post('/api/integrations/timeback/end-activity', async c => {
|
|
38
|
+
return c.json({
|
|
39
|
+
status: 'ok',
|
|
40
|
+
__playcademyDevWarning: 'timeback-not-configured',
|
|
41
|
+
})
|
|
42
|
+
})
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
// TODO: Auth integration
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { Context } from 'hono'
|
|
8
|
-
import type {
|
|
8
|
+
import type { PlaycademyConfig } from '@playcademy/sdk/server'
|
|
9
9
|
import type { RouteMetadata } from '../entry/types'
|
|
10
10
|
import type { HonoEnv, ServerEnv } from '../types'
|
|
11
11
|
|
|
@@ -49,16 +49,27 @@ function formatRoutes(routes: RouteMetadata[]): Array<{ path: string; methods: s
|
|
|
49
49
|
/**
|
|
50
50
|
* Get TimeBack debug info if applicable
|
|
51
51
|
*/
|
|
52
|
-
function getTimebackDebugInfo(config?: PlaycademyConfig
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
function getTimebackDebugInfo(config?: PlaycademyConfig) {
|
|
53
|
+
const timeback = config?.integrations?.timeback
|
|
54
|
+
|
|
55
|
+
if (!timeback || !timeback.courses || timeback.courses.length === 0) {
|
|
56
|
+
return {}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const grades = Array.from(new Set(timeback.courses.map(c => c.grade))).sort()
|
|
60
|
+
const subjects = Array.from(new Set(timeback.courses.map(c => c.subject))).sort()
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
timeback: {
|
|
64
|
+
courseCount: timeback.courses.length,
|
|
65
|
+
grades,
|
|
66
|
+
subjects,
|
|
67
|
+
},
|
|
55
68
|
}
|
|
56
|
-
return {}
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
export async function GET(c: Context<HonoEnv>): Promise<Response> {
|
|
60
72
|
const config = c.get('config')
|
|
61
|
-
const sdk = c.get('sdk')
|
|
62
73
|
const routeMetadata = c.get('routeMetadata')
|
|
63
74
|
|
|
64
75
|
return c.json({
|
|
@@ -68,6 +79,6 @@ export async function GET(c: Context<HonoEnv>): Promise<Response> {
|
|
|
68
79
|
secrets: getSecretsCount(c.env),
|
|
69
80
|
integrations: getEnabledIntegrations(config),
|
|
70
81
|
routes: formatRoutes(routeMetadata),
|
|
71
|
-
...getTimebackDebugInfo(config
|
|
82
|
+
...getTimebackDebugInfo(config),
|
|
72
83
|
})
|
|
73
84
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { verifyGameToken } from '@playcademy/sdk/server'
|
|
2
|
-
|
|
3
1
|
import type { Context } from 'hono'
|
|
4
2
|
import type { PlaycademyConfig } from '@playcademy/sdk/server'
|
|
5
3
|
import type { ActivityData } from '@playcademy/timeback/types'
|
|
@@ -26,82 +24,148 @@ function getConfig(c: Context<HonoEnv>): PlaycademyConfig {
|
|
|
26
24
|
return config
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
function
|
|
30
|
-
activityData
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
function validateRequestBody(body: {
|
|
28
|
+
activityData?: ActivityData
|
|
29
|
+
scoreData?: { correctQuestions?: number; totalQuestions?: number }
|
|
30
|
+
timingData?: { durationSeconds?: number }
|
|
31
|
+
masteredUnits?: number
|
|
32
|
+
}): { error: string } | null {
|
|
33
|
+
if (!body.activityData?.activityId) {
|
|
34
|
+
return { error: 'activityId is required' }
|
|
35
|
+
}
|
|
36
|
+
if (!body.activityData?.grade) {
|
|
37
|
+
return { error: 'grade is required' }
|
|
38
|
+
}
|
|
39
|
+
if (!body.activityData?.subject) {
|
|
40
|
+
return { error: 'subject is required' }
|
|
41
|
+
}
|
|
42
|
+
if (
|
|
43
|
+
typeof body.scoreData?.correctQuestions !== 'number' ||
|
|
44
|
+
typeof body.scoreData?.totalQuestions !== 'number'
|
|
45
|
+
) {
|
|
46
|
+
return { error: 'correctQuestions and totalQuestions are required' }
|
|
47
|
+
}
|
|
48
|
+
if (typeof body.timingData?.durationSeconds !== 'number') {
|
|
49
|
+
return { error: 'durationSeconds is required' }
|
|
50
|
+
}
|
|
51
|
+
if (
|
|
52
|
+
body.masteredUnits !== undefined &&
|
|
53
|
+
(typeof body.masteredUnits !== 'number' || body.masteredUnits < 0)
|
|
54
|
+
) {
|
|
55
|
+
return { error: 'masteredUnits must be a non-negative number when provided' }
|
|
56
|
+
}
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function validateCourse(params: {
|
|
61
|
+
grade: number
|
|
62
|
+
subject: string
|
|
63
|
+
config: PlaycademyConfig
|
|
64
|
+
}): { error: string } | null {
|
|
65
|
+
const { grade, subject, config } = params
|
|
66
|
+
const timebackConfig = config.integrations?.timeback
|
|
67
|
+
const configuredCourse = timebackConfig?.courses?.find(
|
|
68
|
+
course => course.grade === grade && course.subject === subject,
|
|
69
|
+
)
|
|
70
|
+
if (!configuredCourse) {
|
|
71
|
+
const configured = timebackConfig?.courses
|
|
72
|
+
?.map(c => `${c.subject} (Grade ${c.grade})`)
|
|
73
|
+
.join(', ')
|
|
74
|
+
return {
|
|
75
|
+
error: `Invalid grade/subject combination: ${subject} (Grade ${grade}). Configured courses: ${configured || 'none'}`,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function enrichActivityData(params: {
|
|
82
|
+
activityData: ActivityData
|
|
83
|
+
config: PlaycademyConfig
|
|
84
|
+
c: Context<HonoEnv>
|
|
85
|
+
}): { data?: ActivityData; error?: string } {
|
|
86
|
+
const { activityData, config, c } = params
|
|
34
87
|
const appName = activityData.appName || config?.name
|
|
35
|
-
const subject =
|
|
36
|
-
activityData.subject ||
|
|
37
|
-
config?.integrations?.timeback?.course?.defaultSubject ||
|
|
38
|
-
config?.integrations?.timeback?.course?.subjects?.[0]
|
|
39
88
|
const sensorUrl = activityData.sensorUrl || new URL(c.req.url).origin
|
|
40
89
|
|
|
41
|
-
if (!appName)
|
|
42
|
-
|
|
43
|
-
|
|
90
|
+
if (!appName) {
|
|
91
|
+
return { error: 'App name is required (missing from activityData and config)' }
|
|
92
|
+
}
|
|
93
|
+
if (!sensorUrl) {
|
|
94
|
+
return { error: 'Sensor URL is required' }
|
|
95
|
+
}
|
|
44
96
|
|
|
45
|
-
return { ...activityData, appName,
|
|
97
|
+
return { data: { ...activityData, appName, sensorUrl } }
|
|
46
98
|
}
|
|
47
99
|
|
|
48
100
|
export async function POST(c: Context<HonoEnv>): Promise<Response> {
|
|
49
101
|
try {
|
|
50
|
-
// 1.
|
|
51
|
-
const
|
|
52
|
-
if (!
|
|
53
|
-
return c.json({ error: 'Unauthorized' }, 401)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const { user } = await verifyGameToken(token, { baseUrl: c.env.PLAYCADEMY_BASE_URL })
|
|
102
|
+
// 1. Get authenticated user from middleware
|
|
103
|
+
const user = c.get('playcademyUser')
|
|
104
|
+
if (!user) return c.json({ error: 'Unauthorized' }, 401)
|
|
57
105
|
|
|
58
106
|
// 2. Ensure user has TimeBack integration
|
|
59
107
|
if (!user.timeback_id) {
|
|
60
|
-
|
|
108
|
+
const message = 'User does not have TimeBack integration'
|
|
109
|
+
console.error('[TimeBack End Activity] Error:', message)
|
|
110
|
+
return c.json({ error: message }, 400)
|
|
61
111
|
}
|
|
62
112
|
|
|
63
113
|
// 3. Parse request body
|
|
64
|
-
const { activityData, scoreData, timingData, xpEarned } = await c.req.json()
|
|
114
|
+
const { activityData, scoreData, timingData, xpEarned, masteredUnits } = await c.req.json()
|
|
65
115
|
|
|
66
116
|
// 4. Validate required fields
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return c.json({ error:
|
|
117
|
+
const bodyValidationError = validateRequestBody({
|
|
118
|
+
activityData,
|
|
119
|
+
scoreData,
|
|
120
|
+
timingData,
|
|
121
|
+
masteredUnits,
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
if (bodyValidationError) {
|
|
125
|
+
const message = bodyValidationError.error
|
|
126
|
+
console.error('[TimeBack End Activity] Error:', message)
|
|
127
|
+
return c.json({ error: message }, 400)
|
|
78
128
|
}
|
|
79
129
|
|
|
80
|
-
// 5. Get config
|
|
130
|
+
// 5. Get config
|
|
81
131
|
const config = getConfig(c)
|
|
82
|
-
const enrichedActivityData = enrichActivityData(activityData, config, c)
|
|
83
132
|
|
|
84
|
-
// 6.
|
|
133
|
+
// 6. Validate grade/subject against configured courses
|
|
134
|
+
const { grade, subject } = activityData
|
|
135
|
+
const courseValidationError = validateCourse({ grade, subject, config })
|
|
136
|
+
|
|
137
|
+
if (courseValidationError) {
|
|
138
|
+
const message = courseValidationError.error
|
|
139
|
+
console.error('[TimeBack End Activity] Error:', message)
|
|
140
|
+
return c.json({ error: message }, 400)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 7. Enrich activity data with required Caliper fields
|
|
144
|
+
const enrichResult = enrichActivityData({ activityData, config, c })
|
|
145
|
+
|
|
146
|
+
if (!enrichResult.data) {
|
|
147
|
+
console.error('[TimeBack End Activity] Error:', enrichResult.error)
|
|
148
|
+
return c.json({ error: enrichResult.error }, 500)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 8. Get SDK client from context (initialized once per Worker, reused across requests)
|
|
85
152
|
const sdk = c.get('sdk')
|
|
86
153
|
|
|
87
|
-
//
|
|
154
|
+
// 9. End activity (SDK calculates XP server-side with attempt tracking)
|
|
88
155
|
const result = await sdk.timeback.endActivity(user.timeback_id, {
|
|
89
|
-
activityData:
|
|
156
|
+
activityData: enrichResult.data,
|
|
90
157
|
scoreData,
|
|
91
158
|
timingData,
|
|
92
159
|
xpEarned,
|
|
160
|
+
masteredUnits,
|
|
93
161
|
})
|
|
94
162
|
|
|
95
163
|
return c.json(result)
|
|
96
164
|
} catch (error) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
103
|
-
},
|
|
104
|
-
500,
|
|
105
|
-
)
|
|
165
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
166
|
+
const stack = error instanceof Error ? error.stack : undefined
|
|
167
|
+
if (message) console.error('[TimeBack End Activity] Error:', message)
|
|
168
|
+
if (stack) console.error('[TimeBack End Activity] Stack:', stack)
|
|
169
|
+
return c.json({ error: 'Failed to end activity', message, stack }, 500)
|
|
106
170
|
}
|
|
107
171
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
/// <reference types="@cloudflare/workers-types" />
|
|
9
9
|
|
|
10
|
+
import type { UserInfo } from '@playcademy/data/types'
|
|
10
11
|
import type { PlaycademyClient, PlaycademyConfig } from '@playcademy/sdk/server'
|
|
11
12
|
import type { RouteMetadata } from './entry/types'
|
|
12
13
|
|
|
@@ -70,6 +71,7 @@ export interface HonoVariables {
|
|
|
70
71
|
sdk: PlaycademyClient
|
|
71
72
|
config: PlaycademyConfig
|
|
72
73
|
routeMetadata: Array<RouteMetadata>
|
|
74
|
+
playcademyUser?: UserInfo
|
|
73
75
|
[key: string]: unknown
|
|
74
76
|
}
|
|
75
77
|
|