playcademy 0.18.0 → 0.18.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 +1 -1
- package/dist/index.d.ts +24 -12
- package/dist/index.js +465 -255
- package/dist/utils.js +558 -351
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/constants/src/achievements.ts +0 -107
- package/dist/constants/src/auth.ts +0 -13
- package/dist/constants/src/character.ts +0 -16
- package/dist/constants/src/domains.ts +0 -50
- package/dist/constants/src/env-vars.ts +0 -20
- package/dist/constants/src/index.ts +0 -18
- package/dist/constants/src/overworld.ts +0 -330
- package/dist/constants/src/system.ts +0 -10
- package/dist/constants/src/timeback.ts +0 -118
- package/dist/constants/src/typescript.ts +0 -21
- package/dist/constants/src/workers.ts +0 -36
- package/dist/edge-play/src/constants.ts +0 -27
- package/dist/edge-play/src/entry/middleware.ts +0 -247
- package/dist/edge-play/src/entry/queue.test.ts +0 -279
- package/dist/edge-play/src/entry/queue.ts +0 -107
- package/dist/edge-play/src/entry/session.ts +0 -45
- package/dist/edge-play/src/entry/setup.ts +0 -78
- package/dist/edge-play/src/entry/types.ts +0 -30
- package/dist/edge-play/src/entry.ts +0 -94
- package/dist/edge-play/src/html.d.ts +0 -5
- package/dist/edge-play/src/index.ts +0 -4
- package/dist/edge-play/src/lib/errors.ts +0 -51
- package/dist/edge-play/src/lib/index.ts +0 -3
- package/dist/edge-play/src/lib/self-dispatch.test.ts +0 -244
- package/dist/edge-play/src/lib/self-dispatch.ts +0 -41
- package/dist/edge-play/src/lib/validation.test.ts +0 -190
- package/dist/edge-play/src/lib/validation.ts +0 -64
- package/dist/edge-play/src/polyfills.js +0 -54
- package/dist/edge-play/src/register-routes.ts +0 -59
- package/dist/edge-play/src/routes/health.ts +0 -104
- package/dist/edge-play/src/routes/index.ts +0 -66
- package/dist/edge-play/src/routes/integrations/timeback/end-activity.ts +0 -181
- package/dist/edge-play/src/routes/integrations/timeback/get-xp.ts +0 -159
- package/dist/edge-play/src/routes/root.html +0 -253
- package/dist/edge-play/src/routes/root.ts +0 -22
- package/dist/edge-play/src/stub-entry.ts +0 -161
- package/dist/edge-play/src/types.ts +0 -124
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Health check endpoint
|
|
3
|
-
* Route: GET /api/health
|
|
4
|
-
* Always included in deployed backends
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { Context } from 'hono'
|
|
8
|
-
|
|
9
|
-
import type { PlaycademyConfig } from '@playcademy/sdk/server'
|
|
10
|
-
|
|
11
|
-
import type { RouteMetadata } from '../entry/types'
|
|
12
|
-
import type { HonoEnv, ServerEnv } from '../types'
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Check if all required environment variables are configured
|
|
16
|
-
*/
|
|
17
|
-
function isEnvConfigured(env: ServerEnv): boolean {
|
|
18
|
-
return (
|
|
19
|
-
Boolean(env.PLAYCADEMY_API_KEY) && Boolean(env.GAME_ID) && Boolean(env.PLAYCADEMY_BASE_URL)
|
|
20
|
-
)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Get count of configured secrets
|
|
25
|
-
*/
|
|
26
|
-
function getSecretsCount(env: ServerEnv): number {
|
|
27
|
-
return env.secrets ? Object.keys(env.secrets).length : 0
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Build list of enabled integration names
|
|
32
|
-
*/
|
|
33
|
-
function getEnabledIntegrations(config?: PlaycademyConfig): string[] {
|
|
34
|
-
const enabled: string[] = []
|
|
35
|
-
|
|
36
|
-
if (config?.integrations?.timeback) {
|
|
37
|
-
enabled.push('timeback')
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (config?.integrations?.customRoutes) {
|
|
41
|
-
enabled.push('customRoutes')
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (config?.integrations?.database) {
|
|
45
|
-
enabled.push('database')
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (config?.integrations?.kv) {
|
|
49
|
-
enabled.push('kv')
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (config?.integrations?.bucket) {
|
|
53
|
-
enabled.push('bucket')
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return enabled
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Format routes into readable objects with separate path and methods
|
|
61
|
-
*/
|
|
62
|
-
function formatRoutes(routes: RouteMetadata[]): { path: string; methods: string[] }[] {
|
|
63
|
-
return routes.map(r => ({
|
|
64
|
-
path: r.path,
|
|
65
|
-
methods: r.methods || ['*'],
|
|
66
|
-
}))
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Get TimeBack debug info if applicable
|
|
71
|
-
*/
|
|
72
|
-
function getTimebackDebugInfo(config?: PlaycademyConfig) {
|
|
73
|
-
const timeback = config?.integrations?.timeback
|
|
74
|
-
|
|
75
|
-
if (!timeback || !timeback.courses || timeback.courses.length === 0) {
|
|
76
|
-
return {}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const grades = [...new Set(timeback.courses.map(c => c.grade))].toSorted()
|
|
80
|
-
const subjects = [...new Set(timeback.courses.map(c => c.subject))].toSorted()
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
timeback: {
|
|
84
|
-
courseCount: timeback.courses.length,
|
|
85
|
-
grades,
|
|
86
|
-
subjects,
|
|
87
|
-
},
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export async function GET(c: Context<HonoEnv>): Promise<Response> {
|
|
92
|
-
const config = c.get('config')
|
|
93
|
-
const routeMetadata = c.get('routeMetadata')
|
|
94
|
-
|
|
95
|
-
return c.json({
|
|
96
|
-
status: 'ok',
|
|
97
|
-
timestamp: new Date().toISOString(),
|
|
98
|
-
env: { configured: isEnvConfigured(c.env) },
|
|
99
|
-
secrets: getSecretsCount(c.env),
|
|
100
|
-
integrations: getEnabledIntegrations(config),
|
|
101
|
-
routes: formatRoutes(routeMetadata),
|
|
102
|
-
...getTimebackDebugInfo(config),
|
|
103
|
-
})
|
|
104
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Route discovery endpoint
|
|
3
|
-
* Route: GET /api
|
|
4
|
-
* Always included - lists all available routes
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { ROUTES } from '../constants'
|
|
8
|
-
|
|
9
|
-
import type { Context } from 'hono'
|
|
10
|
-
|
|
11
|
-
import type { PlaycademyConfig } from '@playcademy/sdk/server'
|
|
12
|
-
|
|
13
|
-
import type { RouteMetadata } from '../entry/types'
|
|
14
|
-
import type { HonoEnv } from '../types'
|
|
15
|
-
|
|
16
|
-
interface RouteInfo {
|
|
17
|
-
path: string
|
|
18
|
-
methods: string[]
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Get built-in routes that are always available
|
|
23
|
-
*/
|
|
24
|
-
function getBuiltinRoutes(): RouteInfo[] {
|
|
25
|
-
return [
|
|
26
|
-
{ path: ROUTES.INDEX, methods: ['GET'] },
|
|
27
|
-
{ path: ROUTES.HEALTH, methods: ['GET'] },
|
|
28
|
-
]
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Get TimeBack integration routes if enabled
|
|
33
|
-
*/
|
|
34
|
-
function getTimebackRoutes(config?: PlaycademyConfig): RouteInfo[] {
|
|
35
|
-
if (config?.integrations?.timeback) {
|
|
36
|
-
return [{ path: ROUTES.TIMEBACK.END_ACTIVITY, methods: ['POST'] }]
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return []
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Format custom routes from build-time metadata
|
|
44
|
-
*/
|
|
45
|
-
function formatCustomRoutes(routes: RouteMetadata[]): RouteInfo[] {
|
|
46
|
-
return routes.map(r => ({
|
|
47
|
-
path: r.path,
|
|
48
|
-
methods: r.methods || ['*'],
|
|
49
|
-
}))
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export async function GET(c: Context<HonoEnv>): Promise<Response> {
|
|
53
|
-
const config = c.get('config')
|
|
54
|
-
const routeMetadata = c.get('routeMetadata')
|
|
55
|
-
|
|
56
|
-
const routes = [
|
|
57
|
-
...getBuiltinRoutes(),
|
|
58
|
-
...getTimebackRoutes(config),
|
|
59
|
-
...formatCustomRoutes(routeMetadata),
|
|
60
|
-
]
|
|
61
|
-
|
|
62
|
-
return c.json({
|
|
63
|
-
name: config.name,
|
|
64
|
-
routes,
|
|
65
|
-
})
|
|
66
|
-
}
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
buildErrorResponse,
|
|
3
|
-
isValidGrade,
|
|
4
|
-
isValidSubject,
|
|
5
|
-
logError,
|
|
6
|
-
VALID_GRADES,
|
|
7
|
-
VALID_SUBJECTS,
|
|
8
|
-
validateCourseConfig,
|
|
9
|
-
} from '../../../lib'
|
|
10
|
-
|
|
11
|
-
import type { Context } from 'hono'
|
|
12
|
-
|
|
13
|
-
import type { PlaycademyConfig } from '@playcademy/sdk/server'
|
|
14
|
-
import type { ActivityData } from '@playcademy/types/timeback'
|
|
15
|
-
|
|
16
|
-
import type { HonoEnv } from '../../../types'
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* TimeBack integration - End activity and submit results
|
|
20
|
-
* Route: POST /api/integrations/timeback/end-activity
|
|
21
|
-
* Auto-generated when integrations.timeback is configured
|
|
22
|
-
*
|
|
23
|
-
* Flow:
|
|
24
|
-
* 1. Game frontend calls this route on deployed backend ({slug}.playcademy.gg/api/integrations/timeback/end-activity)
|
|
25
|
-
* 2. This route calls Playcademy platform API via SDK (hub.playcademy.net/api/timeback/end-activity)
|
|
26
|
-
* 3. Platform API sends both ActivityEvent and TimeSpent Caliper events to TimeBack
|
|
27
|
-
*
|
|
28
|
-
* This acts as a secure proxy - game developers don't need TimeBack credentials or
|
|
29
|
-
* their own backend infrastructure. The SDK handles config enrichment and metadata.
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
function getConfig(c: Context<HonoEnv>): PlaycademyConfig {
|
|
33
|
-
const config = c.get('config')
|
|
34
|
-
const timebackConfig = config?.integrations?.timeback
|
|
35
|
-
|
|
36
|
-
if (!timebackConfig) {
|
|
37
|
-
throw new Error('TimeBack integration not found')
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return config
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function validateRequestBody(body: {
|
|
44
|
-
activityData?: ActivityData
|
|
45
|
-
scoreData?: { correctQuestions?: number; totalQuestions?: number }
|
|
46
|
-
timingData?: { durationSeconds?: number }
|
|
47
|
-
masteredUnits?: number
|
|
48
|
-
}): { error: string } | null {
|
|
49
|
-
if (!body.activityData?.activityId) {
|
|
50
|
-
return { error: 'activityId is required' }
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (!isValidGrade(body.activityData?.grade)) {
|
|
54
|
-
return { error: `grade must be a valid grade level (${VALID_GRADES.join(', ')})` }
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (!isValidSubject(body.activityData?.subject)) {
|
|
58
|
-
return { error: `subject must be a valid subject (${VALID_SUBJECTS.join(', ')})` }
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
typeof body.scoreData?.correctQuestions !== 'number' ||
|
|
63
|
-
typeof body.scoreData?.totalQuestions !== 'number'
|
|
64
|
-
) {
|
|
65
|
-
return { error: 'correctQuestions and totalQuestions are required' }
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (typeof body.timingData?.durationSeconds !== 'number') {
|
|
69
|
-
return { error: 'durationSeconds is required' }
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
body.masteredUnits !== undefined &&
|
|
74
|
-
(typeof body.masteredUnits !== 'number' || body.masteredUnits < 0)
|
|
75
|
-
) {
|
|
76
|
-
return { error: 'masteredUnits must be a non-negative number when provided' }
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return null
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function enrichActivityData(params: {
|
|
83
|
-
activityData: ActivityData
|
|
84
|
-
config: PlaycademyConfig
|
|
85
|
-
c: Context<HonoEnv>
|
|
86
|
-
}): { data?: ActivityData; error?: string } {
|
|
87
|
-
const { activityData, config, c } = params
|
|
88
|
-
const appName = activityData.appName || config?.name
|
|
89
|
-
const sensorUrl = activityData.sensorUrl || new URL(c.req.url).origin
|
|
90
|
-
|
|
91
|
-
if (!appName) {
|
|
92
|
-
return { error: 'App name is required (missing from activityData and config)' }
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (!sensorUrl) {
|
|
96
|
-
return { error: 'Sensor URL is required' }
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return { data: { ...activityData, appName, sensorUrl } }
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export async function POST(c: Context<HonoEnv>): Promise<Response> {
|
|
103
|
-
try {
|
|
104
|
-
// 1. Get authenticated user from middleware
|
|
105
|
-
const user = c.get('playcademyUser')
|
|
106
|
-
|
|
107
|
-
if (!user) {
|
|
108
|
-
return c.json({ error: 'Unauthorized' }, 401)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// 2. Ensure user has TimeBack integration
|
|
112
|
-
if (!user.timeback_id) {
|
|
113
|
-
const message = 'User does not have TimeBack integration'
|
|
114
|
-
|
|
115
|
-
console.error('[TimeBack End Activity] Error:', message)
|
|
116
|
-
|
|
117
|
-
return c.json({ error: message }, 400)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// 3. Parse request body
|
|
121
|
-
const { activityData, scoreData, timingData, xpEarned, masteredUnits } = await c.req.json()
|
|
122
|
-
|
|
123
|
-
// 4. Validate required fields
|
|
124
|
-
const bodyValidationError = validateRequestBody({
|
|
125
|
-
activityData,
|
|
126
|
-
scoreData,
|
|
127
|
-
timingData,
|
|
128
|
-
masteredUnits,
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
if (bodyValidationError) {
|
|
132
|
-
const message = bodyValidationError.error
|
|
133
|
-
|
|
134
|
-
console.error('[TimeBack End Activity] Error:', message)
|
|
135
|
-
|
|
136
|
-
return c.json({ error: message }, 400)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// 5. Get config
|
|
140
|
-
const config = getConfig(c)
|
|
141
|
-
|
|
142
|
-
// 6. Validate grade/subject against configured courses
|
|
143
|
-
const { grade, subject } = activityData
|
|
144
|
-
const courseValidationError = validateCourseConfig({ grade, subject, config })
|
|
145
|
-
|
|
146
|
-
if (courseValidationError) {
|
|
147
|
-
const message = courseValidationError.error
|
|
148
|
-
|
|
149
|
-
console.error('[TimeBack End Activity] Error:', message)
|
|
150
|
-
|
|
151
|
-
return c.json({ error: message }, 400)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// 7. Enrich activity data with required Caliper fields
|
|
155
|
-
const enrichResult = enrichActivityData({ activityData, config, c })
|
|
156
|
-
|
|
157
|
-
if (!enrichResult.data) {
|
|
158
|
-
console.error('[TimeBack End Activity] Error:', enrichResult.error)
|
|
159
|
-
|
|
160
|
-
return c.json({ error: enrichResult.error }, 500)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// 8. Get SDK client from context (initialized once per Worker, reused across requests)
|
|
164
|
-
const sdk = c.get('sdk')
|
|
165
|
-
|
|
166
|
-
// 9. End activity (SDK calculates XP server-side with attempt tracking)
|
|
167
|
-
const result = await sdk.timeback.endActivity(user.timeback_id, {
|
|
168
|
-
activityData: enrichResult.data,
|
|
169
|
-
scoreData,
|
|
170
|
-
timingData,
|
|
171
|
-
xpEarned,
|
|
172
|
-
masteredUnits,
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
return c.json(result)
|
|
176
|
-
} catch (error) {
|
|
177
|
-
logError('TimeBack End Activity', error)
|
|
178
|
-
|
|
179
|
-
return c.json(buildErrorResponse(c, error, 'Failed to end activity'), 500)
|
|
180
|
-
}
|
|
181
|
-
}
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
buildErrorResponse,
|
|
3
|
-
isValidGrade,
|
|
4
|
-
isValidSubject,
|
|
5
|
-
logError,
|
|
6
|
-
VALID_GRADES,
|
|
7
|
-
VALID_SUBJECTS,
|
|
8
|
-
validateCourseConfig,
|
|
9
|
-
} from '../../../lib'
|
|
10
|
-
|
|
11
|
-
import type { Context } from 'hono'
|
|
12
|
-
|
|
13
|
-
import type { PlaycademyConfig } from '@playcademy/sdk/server'
|
|
14
|
-
|
|
15
|
-
import type { HonoEnv } from '../../../types'
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* TimeBack integration - Get student XP
|
|
19
|
-
* Route: GET /api/integrations/timeback/xp
|
|
20
|
-
* Auto-generated when integrations.timeback is configured
|
|
21
|
-
*
|
|
22
|
-
* Flow:
|
|
23
|
-
* 1. Game frontend calls this route on deployed backend ({slug}.playcademy.gg/api/integrations/timeback/xp)
|
|
24
|
-
* 2. This route calls Playcademy platform API via SDK (hub.playcademy.net/api/timeback/student-xp/:timebackId)
|
|
25
|
-
* 3. Platform API fetches XP data from TimeBack EduBridge analytics API
|
|
26
|
-
*
|
|
27
|
-
* By default, returns XP for all courses configured for this game/app.
|
|
28
|
-
* Use `grade` and `subject` to query a specific course instead.
|
|
29
|
-
*
|
|
30
|
-
* Query Parameters:
|
|
31
|
-
*
|
|
32
|
-
* @param {number} [grade] - Grade level to filter (-1 to 13).
|
|
33
|
-
* Must be used together with `subject`.
|
|
34
|
-
* Example: `?grade=3&subject=Math`
|
|
35
|
-
*
|
|
36
|
-
* @param {string} [subject] - Subject to filter.
|
|
37
|
-
* Must be used together with `grade`.
|
|
38
|
-
* Valid values: Reading, Language, Vocabulary, Social Studies, Writing, Science, FastMath, Math, None
|
|
39
|
-
* Example: `?grade=3&subject=Math`
|
|
40
|
-
*
|
|
41
|
-
* @param {string} [include] - Comma-separated list of additional data to include.
|
|
42
|
-
* Available options:
|
|
43
|
-
* - `perCourse` - Include per-course XP breakdown with grade, subject, and xp
|
|
44
|
-
* - `today` - Include today's XP (both total and per-course if perCourse is set)
|
|
45
|
-
* Example: `?include=perCourse,today`
|
|
46
|
-
*
|
|
47
|
-
* Response:
|
|
48
|
-
* ```json
|
|
49
|
-
* {
|
|
50
|
-
* "totalXp": 1500,
|
|
51
|
-
* "todayXp": 50, // Only if include contains 'today'
|
|
52
|
-
* "courses": [ // Only if include contains 'perCourse'
|
|
53
|
-
* {
|
|
54
|
-
* "grade": 3,
|
|
55
|
-
* "subject": "Math",
|
|
56
|
-
* "title": "Math Grade 3",
|
|
57
|
-
* "totalXp": 1000,
|
|
58
|
-
* "todayXp": 30 // Only if include contains 'today'
|
|
59
|
-
* }
|
|
60
|
-
* ]
|
|
61
|
-
* }
|
|
62
|
-
* ```
|
|
63
|
-
*/
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Get config and validate that the game has TimeBack configured.
|
|
67
|
-
*/
|
|
68
|
-
function getConfig(c: Context<HonoEnv>): PlaycademyConfig {
|
|
69
|
-
const config = c.get('config')
|
|
70
|
-
const timebackConfig = config?.integrations?.timeback
|
|
71
|
-
|
|
72
|
-
if (!timebackConfig) {
|
|
73
|
-
throw new Error('TimeBack integration not found')
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return config
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export async function GET(c: Context<HonoEnv>): Promise<Response> {
|
|
80
|
-
try {
|
|
81
|
-
// 1. Get authenticated user from middleware
|
|
82
|
-
const user = c.get('playcademyUser')
|
|
83
|
-
|
|
84
|
-
if (!user) {
|
|
85
|
-
return c.json({ error: 'Unauthorized' }, 401)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// 2. Ensure user has TimeBack integration
|
|
89
|
-
if (!user.timeback_id) {
|
|
90
|
-
const message = 'User does not have TimeBack integration'
|
|
91
|
-
|
|
92
|
-
console.error('[TimeBack Get XP] Error:', message)
|
|
93
|
-
|
|
94
|
-
return c.json({ error: message }, 400)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// 3. Validate TimeBack is configured for this game
|
|
98
|
-
const config = getConfig(c)
|
|
99
|
-
|
|
100
|
-
// 4. Parse query parameters
|
|
101
|
-
const url = new URL(c.req.url)
|
|
102
|
-
const gradeParam = url.searchParams.get('grade')
|
|
103
|
-
const subjectParam = url.searchParams.get('subject')
|
|
104
|
-
const includeParam = url.searchParams.get('include')
|
|
105
|
-
const include = includeParam
|
|
106
|
-
? (includeParam.split(',').map(s => s.trim()) as ('perCourse' | 'today')[])
|
|
107
|
-
: undefined
|
|
108
|
-
|
|
109
|
-
let grade: number | undefined
|
|
110
|
-
let subject: string | undefined
|
|
111
|
-
|
|
112
|
-
if (gradeParam !== null || subjectParam !== null) {
|
|
113
|
-
if (gradeParam === null || subjectParam === null) {
|
|
114
|
-
return c.json({ error: 'Both grade and subject must be provided together' }, 400)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
grade = parseInt(gradeParam, 10)
|
|
118
|
-
subject = subjectParam
|
|
119
|
-
|
|
120
|
-
if (!isValidGrade(grade)) {
|
|
121
|
-
return c.json(
|
|
122
|
-
{ error: `grade must be a valid grade level (${VALID_GRADES.join(', ')})` },
|
|
123
|
-
400,
|
|
124
|
-
)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (!isValidSubject(subject)) {
|
|
128
|
-
return c.json(
|
|
129
|
-
{ error: `subject must be a valid subject (${VALID_SUBJECTS.join(', ')})` },
|
|
130
|
-
400,
|
|
131
|
-
)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// 5. Validate grade/subject against configured courses
|
|
135
|
-
const courseValidationError = validateCourseConfig({ grade, subject, config })
|
|
136
|
-
|
|
137
|
-
if (courseValidationError) {
|
|
138
|
-
return c.json({ error: courseValidationError.error }, 400)
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// 6. Get SDK client from context
|
|
143
|
-
const sdk = c.get('sdk')
|
|
144
|
-
|
|
145
|
-
// 7. Fetch student XP from platform API
|
|
146
|
-
// The SDK automatically passes gameId, so XP is filtered to this game's courses
|
|
147
|
-
const result = await sdk.timeback.getStudentXp(user.timeback_id, {
|
|
148
|
-
grade,
|
|
149
|
-
subject,
|
|
150
|
-
include,
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
return c.json(result)
|
|
154
|
-
} catch (error) {
|
|
155
|
-
logError('TimeBack Get XP', error)
|
|
156
|
-
|
|
157
|
-
return c.json(buildErrorResponse(c, error, 'Failed to get XP'), 500)
|
|
158
|
-
}
|
|
159
|
-
}
|