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.
Files changed (43) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/index.d.ts +24 -12
  3. package/dist/index.js +465 -255
  4. package/dist/utils.js +558 -351
  5. package/dist/version.js +1 -1
  6. package/package.json +1 -1
  7. package/dist/constants/src/achievements.ts +0 -107
  8. package/dist/constants/src/auth.ts +0 -13
  9. package/dist/constants/src/character.ts +0 -16
  10. package/dist/constants/src/domains.ts +0 -50
  11. package/dist/constants/src/env-vars.ts +0 -20
  12. package/dist/constants/src/index.ts +0 -18
  13. package/dist/constants/src/overworld.ts +0 -330
  14. package/dist/constants/src/system.ts +0 -10
  15. package/dist/constants/src/timeback.ts +0 -118
  16. package/dist/constants/src/typescript.ts +0 -21
  17. package/dist/constants/src/workers.ts +0 -36
  18. package/dist/edge-play/src/constants.ts +0 -27
  19. package/dist/edge-play/src/entry/middleware.ts +0 -247
  20. package/dist/edge-play/src/entry/queue.test.ts +0 -279
  21. package/dist/edge-play/src/entry/queue.ts +0 -107
  22. package/dist/edge-play/src/entry/session.ts +0 -45
  23. package/dist/edge-play/src/entry/setup.ts +0 -78
  24. package/dist/edge-play/src/entry/types.ts +0 -30
  25. package/dist/edge-play/src/entry.ts +0 -94
  26. package/dist/edge-play/src/html.d.ts +0 -5
  27. package/dist/edge-play/src/index.ts +0 -4
  28. package/dist/edge-play/src/lib/errors.ts +0 -51
  29. package/dist/edge-play/src/lib/index.ts +0 -3
  30. package/dist/edge-play/src/lib/self-dispatch.test.ts +0 -244
  31. package/dist/edge-play/src/lib/self-dispatch.ts +0 -41
  32. package/dist/edge-play/src/lib/validation.test.ts +0 -190
  33. package/dist/edge-play/src/lib/validation.ts +0 -64
  34. package/dist/edge-play/src/polyfills.js +0 -54
  35. package/dist/edge-play/src/register-routes.ts +0 -59
  36. package/dist/edge-play/src/routes/health.ts +0 -104
  37. package/dist/edge-play/src/routes/index.ts +0 -66
  38. package/dist/edge-play/src/routes/integrations/timeback/end-activity.ts +0 -181
  39. package/dist/edge-play/src/routes/integrations/timeback/get-xp.ts +0 -159
  40. package/dist/edge-play/src/routes/root.html +0 -253
  41. package/dist/edge-play/src/routes/root.ts +0 -22
  42. package/dist/edge-play/src/stub-entry.ts +0 -161
  43. 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
- }