playcademy 0.11.4 → 0.11.6

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.
@@ -13,15 +13,14 @@ export { WORKER_ENV_VARS as ENV_VARS, WORKER_NAMING }
13
13
  * Built-in API routes
14
14
  */
15
15
  export const ROUTES = {
16
- /** Health check endpoint */
17
- HEALTH: '/api/health',
18
16
  /** Route index (lists available routes) */
19
17
  INDEX: '/api',
20
18
 
19
+ /** Health check endpoint */
20
+ HEALTH: '/api/health',
21
+
21
22
  /** TimeBack integration routes */
22
23
  TIMEBACK: {
23
- PROGRESS: `/api${TIMEBACK_ROUTES.PROGRESS}`,
24
- SESSION_END: `/api${TIMEBACK_ROUTES.SESSION_END}`,
25
- AWARD_XP: `/api${TIMEBACK_ROUTES.AWARD_XP}`,
24
+ END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}`,
26
25
  },
27
26
  } as const
@@ -97,7 +97,7 @@ app.use('*', async (c, next) => {
97
97
  * Register built-in integration routes based on enabled integrations
98
98
  *
99
99
  * This function conditionally imports and registers routes like:
100
- * - POST /api/integrations/timeback/progress (if timeback enabled)
100
+ * - POST /api/integrations/timeback/end-activity (if timeback enabled)
101
101
  * - GET /api/health (always included)
102
102
  *
103
103
  * Uses dynamic imports for tree-shaking: if an integration is not enabled,
@@ -31,15 +31,12 @@ export async function registerBuiltinRoutes(app: Hono<HonoEnv>, integrations?: I
31
31
 
32
32
  // TimeBack integration
33
33
  if (integrations?.timeback) {
34
- const [progress, sessionEnd, awardXp] = await Promise.all([
35
- import('./routes/integrations/timeback/progress'),
36
- import('./routes/integrations/timeback/session-end'),
37
- import('./routes/integrations/timeback/award-xp'),
34
+ const [endActivity] = await Promise.all([
35
+ import('./routes/integrations/timeback/end-activity'),
36
+ // ... other routes
38
37
  ])
39
-
40
- app.post(ROUTES.TIMEBACK.PROGRESS, progress.POST)
41
- app.post(ROUTES.TIMEBACK.SESSION_END, sessionEnd.POST)
42
- app.post(ROUTES.TIMEBACK.AWARD_XP, awardXp.POST)
38
+ app.post(ROUTES.TIMEBACK.END_ACTIVITY, endActivity.POST)
39
+ // ... other routes
43
40
  }
44
41
 
45
42
  // TODO: Auth integration
@@ -19,11 +19,7 @@ export async function GET(c: Context<HonoEnv>): Promise<Response> {
19
19
 
20
20
  // Add TimeBack routes if configured
21
21
  if (config.integrations?.timeback) {
22
- routes.push(
23
- `POST ${ROUTES.TIMEBACK.PROGRESS}`,
24
- `POST ${ROUTES.TIMEBACK.SESSION_END}`,
25
- `POST ${ROUTES.TIMEBACK.AWARD_XP}`,
26
- )
22
+ routes.push(`POST ${ROUTES.TIMEBACK.END_ACTIVITY}`)
27
23
  }
28
24
 
29
25
  // Add custom routes
@@ -1,18 +1,19 @@
1
1
  import { verifyGameToken } from '@playcademy/sdk/server'
2
2
 
3
3
  import type { Context } from 'hono'
4
- import type { PlaycademyConfig, SessionData } from '@playcademy/sdk/server'
4
+ import type { PlaycademyConfig } from '@playcademy/sdk/server'
5
+ import type { ActivityData } from '@playcademy/timeback/types'
5
6
  import type { HonoEnv } from '../../../types'
6
7
 
7
8
  /**
8
- * TimeBack integration - Record session end
9
- * Route: POST /api/integrations/timeback/session-end
9
+ * TimeBack integration - End activity and submit results
10
+ * Route: POST /api/integrations/timeback/end-activity
10
11
  * Auto-generated when integrations.timeback is configured
11
12
  *
12
13
  * Flow:
13
- * 1. Game frontend calls this route on deployed backend ({slug}.playcademy.gg/api/integrations/timeback/session-end)
14
- * 2. This route calls Playcademy platform API via SDK (hub.playcademy.net/api/timeback/session-end)
15
- * 3. Platform API sends Caliper events to TimeBack
14
+ * 1. Game frontend calls this route on deployed backend ({slug}.playcademy.gg/api/integrations/timeback/end-activity)
15
+ * 2. This route calls Playcademy platform API via SDK (hub.playcademy.net/api/timeback/end-activity)
16
+ * 3. Platform API sends both ActivityEvent and TimeSpent Caliper events to TimeBack
16
17
  *
17
18
  * This acts as a secure proxy - game developers don't need TimeBack credentials or
18
19
  * their own backend infrastructure. The SDK handles config enrichment and metadata.
@@ -25,23 +26,23 @@ function getConfig(c: Context<HonoEnv>): PlaycademyConfig {
25
26
  return config
26
27
  }
27
28
 
28
- function enrichSessionData(
29
- sessionData: SessionData,
29
+ function enrichActivityData(
30
+ activityData: ActivityData,
30
31
  config: PlaycademyConfig,
31
32
  c: Context<HonoEnv>,
32
- ): SessionData {
33
- const appName = sessionData.appName || config?.name
33
+ ): ActivityData {
34
+ const appName = activityData.appName || config?.name
34
35
  const subject =
35
- sessionData.subject ||
36
+ activityData.subject ||
36
37
  config?.integrations?.timeback?.course?.defaultSubject ||
37
38
  config?.integrations?.timeback?.course?.subjects?.[0]
38
- const sensorUrl = sessionData.sensorUrl || new URL(c.req.url).origin
39
+ const sensorUrl = activityData.sensorUrl || new URL(c.req.url).origin
39
40
 
40
41
  if (!appName) throw new Error('App name is required')
41
42
  if (!subject) throw new Error('Subject is required')
42
43
  if (!sensorUrl) throw new Error('Sensor URL is required')
43
44
 
44
- return { ...sessionData, appName, subject, sensorUrl }
45
+ return { ...activityData, appName, subject, sensorUrl }
45
46
  }
46
47
 
47
48
  export async function POST(c: Context<HonoEnv>): Promise<Response> {
@@ -52,7 +53,7 @@ export async function POST(c: Context<HonoEnv>): Promise<Response> {
52
53
  return c.json({ error: 'Unauthorized' }, 401)
53
54
  }
54
55
 
55
- const { user } = await verifyGameToken(token)
56
+ const { user } = await verifyGameToken(token, { baseUrl: c.env.PLAYCADEMY_BASE_URL })
56
57
 
57
58
  // 2. Ensure user has TimeBack integration
58
59
  if (!user.timeback_id) {
@@ -60,26 +61,43 @@ export async function POST(c: Context<HonoEnv>): Promise<Response> {
60
61
  }
61
62
 
62
63
  // 3. Parse request body
63
- const { sessionData } = await c.req.json()
64
+ const { activityData, scoreData, timingData, xpEarned } = await c.req.json()
64
65
 
65
- // 4. Get config and enrich session data with required Caliper fields
66
- const config = getConfig(c)
66
+ // 4. Validate required fields
67
+ if (!activityData?.activityId) {
68
+ return c.json({ error: 'activityId is required' }, 400)
69
+ }
70
+ if (
71
+ typeof scoreData?.correctQuestions !== 'number' ||
72
+ typeof scoreData?.totalQuestions !== 'number'
73
+ ) {
74
+ return c.json({ error: 'correctQuestions and totalQuestions are required' }, 400)
75
+ }
76
+ if (typeof timingData?.durationSeconds !== 'number') {
77
+ return c.json({ error: 'durationSeconds is required' }, 400)
78
+ }
67
79
 
68
- // 5. Enrich session data with required Caliper fields
69
- const enrichedSessionData = enrichSessionData(sessionData, config, c)
80
+ // 5. Get config and enrich activity data with required Caliper fields
81
+ const config = getConfig(c)
82
+ const enrichedActivityData = enrichActivityData(activityData, config, c)
70
83
 
71
84
  // 6. Get SDK client from context (initialized once per Worker, reused across requests)
72
85
  const sdk = c.get('sdk')
73
86
 
74
- // 7. Record session timing to TimeBack (SDK handles enrichment & Caliper events)
75
- const result = await sdk.timeback.recordSessionEnd(user.timeback_id, enrichedSessionData)
87
+ // 7. End activity (SDK calculates XP server-side with attempt tracking)
88
+ const result = await sdk.timeback.endActivity(user.timeback_id, {
89
+ activityData: enrichedActivityData,
90
+ scoreData,
91
+ timingData,
92
+ xpEarned,
93
+ })
76
94
 
77
95
  return c.json(result)
78
96
  } catch (error) {
79
- console.error('[TimeBack Session End] Error:', error)
97
+ console.error('[TimeBack End Activity] Error:', error)
80
98
  return c.json(
81
99
  {
82
- error: 'Failed to record session end',
100
+ error: 'Failed to end activity',
83
101
  message: error instanceof Error ? error.message : String(error),
84
102
  stack: error instanceof Error ? error.stack : undefined,
85
103
  },