playcademy 0.11.3 → 0.11.5
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/edge-play/src/constants.ts +7 -4
- package/dist/edge-play/src/entry.ts +2 -2
- package/dist/edge-play/src/index.ts +1 -0
- package/dist/edge-play/src/register-routes.ts +5 -8
- package/dist/edge-play/src/routes/index.ts +8 -6
- package/dist/edge-play/src/routes/integrations/timeback/{session-end.ts → end-activity.ts} +41 -23
- package/dist/index.js +3431 -2820
- package/dist/types.d.ts +61 -3
- package/dist/utils.d.ts +5 -1
- package/dist/utils.js +198 -194
- package/package.json +2 -2
- package/dist/edge-play/src/routes/integrations/timeback/award-xp.ts +0 -87
- package/dist/edge-play/src/routes/integrations/timeback/progress.ts +0 -89
|
@@ -13,11 +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
|
|
|
21
|
-
/**
|
|
22
|
-
|
|
19
|
+
/** Health check endpoint */
|
|
20
|
+
HEALTH: '/api/health',
|
|
21
|
+
|
|
22
|
+
/** TimeBack integration routes */
|
|
23
|
+
TIMEBACK: {
|
|
24
|
+
END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}`,
|
|
25
|
+
},
|
|
23
26
|
} as const
|
|
@@ -97,12 +97,12 @@ 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/
|
|
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,
|
|
104
104
|
* its route code is completely removed from the bundle.
|
|
105
105
|
*/
|
|
106
|
-
registerBuiltinRoutes(app, PLAYCADEMY_CONFIG.integrations)
|
|
106
|
+
await registerBuiltinRoutes(app, PLAYCADEMY_CONFIG.integrations)
|
|
107
107
|
|
|
108
108
|
export default app
|
|
@@ -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 [
|
|
35
|
-
import('./routes/integrations/timeback/
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
@@ -2,22 +2,24 @@
|
|
|
2
2
|
* Route discovery endpoint
|
|
3
3
|
* Route: GET /api
|
|
4
4
|
* Always included - lists all available routes
|
|
5
|
+
*
|
|
6
|
+
* NOTE: This pulls from the actual ROUTES constants used for registration,
|
|
7
|
+
* ensuring the advertised routes match what's actually deployed.
|
|
5
8
|
*/
|
|
6
9
|
|
|
10
|
+
import { ROUTES } from '../constants'
|
|
11
|
+
|
|
7
12
|
import type { Context } from 'hono'
|
|
8
13
|
import type { HonoEnv } from '../types'
|
|
9
14
|
|
|
10
15
|
export async function GET(c: Context<HonoEnv>): Promise<Response> {
|
|
11
16
|
const config = c.get('config')
|
|
12
17
|
const customRoutes = c.get('customRoutes') || []
|
|
13
|
-
const routes: string[] = [
|
|
18
|
+
const routes: string[] = [`GET ${ROUTES.INDEX}`, `GET ${ROUTES.HEALTH}`]
|
|
14
19
|
|
|
20
|
+
// Add TimeBack routes if configured
|
|
15
21
|
if (config.integrations?.timeback) {
|
|
16
|
-
routes.push(
|
|
17
|
-
'POST /api/integrations/timeback/progress',
|
|
18
|
-
'POST /api/integrations/timeback/session-end',
|
|
19
|
-
'POST /api/integrations/timeback/award-xp',
|
|
20
|
-
)
|
|
22
|
+
routes.push(`POST ${ROUTES.TIMEBACK.END_ACTIVITY}`)
|
|
21
23
|
}
|
|
22
24
|
|
|
23
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
|
|
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 -
|
|
9
|
-
* Route: POST /api/integrations/timeback/
|
|
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/
|
|
14
|
-
* 2. This route calls Playcademy platform API via SDK (hub.playcademy.net/api/timeback/
|
|
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
|
|
29
|
-
|
|
29
|
+
function enrichActivityData(
|
|
30
|
+
activityData: ActivityData,
|
|
30
31
|
config: PlaycademyConfig,
|
|
31
32
|
c: Context<HonoEnv>,
|
|
32
|
-
):
|
|
33
|
-
const appName =
|
|
33
|
+
): ActivityData {
|
|
34
|
+
const appName = activityData.appName || config?.name
|
|
34
35
|
const subject =
|
|
35
|
-
|
|
36
|
+
activityData.subject ||
|
|
36
37
|
config?.integrations?.timeback?.course?.defaultSubject ||
|
|
37
38
|
config?.integrations?.timeback?.course?.subjects?.[0]
|
|
38
|
-
const sensorUrl =
|
|
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 { ...
|
|
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 {
|
|
64
|
+
const { activityData, scoreData, timingData, xpEarned } = await c.req.json()
|
|
64
65
|
|
|
65
|
-
// 4.
|
|
66
|
-
|
|
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.
|
|
69
|
-
const
|
|
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.
|
|
75
|
-
const result = await sdk.timeback.
|
|
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
|
|
97
|
+
console.error('[TimeBack End Activity] Error:', error)
|
|
80
98
|
return c.json(
|
|
81
99
|
{
|
|
82
|
-
error: 'Failed to
|
|
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
|
},
|