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,118 +0,0 @@
1
- /**
2
- * TimeBack Integration Constants
3
- *
4
- * Constants related to TimeBack Caliper integration.
5
- */
6
-
7
- /**
8
- * TimeBack LTI issuer URLs by environment
9
- * Used for JWT token verification (iss claim)
10
- */
11
- export const TIMEBACK_LTI_ISSUER_URLS = {
12
- production: 'https://timeback.com',
13
- staging: 'https://staging.timeback.com',
14
- } as const
15
-
16
- /**
17
- * TimeBack Caliper sensor URLs
18
- * Allowed sensor URLs for Caliper event validation
19
- */
20
- export const TIMEBACK_CALIPER_SENSORS = [
21
- 'https://samuraimath.com',
22
- 'https://app.alphamath.school',
23
- 'https://mathraiders.com',
24
- ] as const
25
-
26
- /**
27
- * TimeBack integration route paths (on game backend)
28
- * These routes are available on game backends when TimeBack is enabled
29
- *
30
- * NOTE:
31
- * These paths are relative to /api/ - the full paths are /api/integrations/timeback/*
32
- */
33
- export const TIMEBACK_ROUTES = {
34
- END_ACTIVITY: '/integrations/timeback/end-activity',
35
- GET_XP: '/integrations/timeback/xp',
36
- } as const
37
-
38
- /**
39
- * TimeBack OneRoster organization identifier
40
- * This is the default organization sourcedId for Playcademy games
41
- */
42
- export const TIMEBACK_ORG_SOURCED_ID = 'PLAYCADEMY' as const
43
-
44
- /**
45
- * Default organization name for Playcademy games
46
- */
47
- export const TIMEBACK_ORG_NAME = 'Playcademy Studios' as const
48
-
49
- /**
50
- * Default organization type for Playcademy games
51
- */
52
- export const TIMEBACK_ORG_TYPE = 'department' as const
53
-
54
- // ============================================================================
55
- // TimeBack Resource Defaults
56
- // ============================================================================
57
-
58
- /**
59
- * Default course configuration values
60
- */
61
- export const TIMEBACK_COURSE_DEFAULTS = {
62
- gradingScheme: 'STANDARD',
63
- level: {
64
- elementary: 'Elementary',
65
- middle: 'Middle',
66
- high: 'High',
67
- ap: 'AP',
68
- },
69
- goals: {
70
- dailyXp: 50,
71
- dailyLessons: 3,
72
- },
73
- metrics: {
74
- totalXp: 1000,
75
- totalLessons: 50,
76
- },
77
- } as const
78
-
79
- /**
80
- * Default resource configuration values
81
- */
82
- export const TIMEBACK_RESOURCE_DEFAULTS = {
83
- vendorId: 'playcademy',
84
- roles: ['primary'] as const,
85
- importance: 'primary' as const,
86
- metadata: {
87
- type: 'interactive' as const,
88
- toolProvider: 'Playcademy',
89
- instructionalMethod: 'exploratory' as const,
90
- language: 'en-US',
91
- },
92
- } as const
93
-
94
- /**
95
- * Default component configuration values
96
- */
97
- export const TIMEBACK_COMPONENT_DEFAULTS = {
98
- sortOrder: 1,
99
- prerequisiteCriteria: 'ALL' as const,
100
- } as const
101
-
102
- /**
103
- * Default componentResource configuration values
104
- */
105
- export const TIMEBACK_COMPONENT_RESOURCE_DEFAULTS = {
106
- sortOrder: 1,
107
- lessonType: 'quiz',
108
- } as const
109
-
110
- // ============================================================================
111
- // 2HL (Two-Hour Learning) Model
112
- // ============================================================================
113
-
114
- /**
115
- * Daily XP threshold for the 2HL model.
116
- * Students must earn this much XP daily to unlock non-enrolled games.
117
- */
118
- export const DAILY_XP_THRESHOLD = 120
@@ -1,21 +0,0 @@
1
- /**
2
- * TypeScript tooling configuration.
3
- *
4
- * Centralizes the TypeScript runner used for workspace type checking.
5
- */
6
-
7
- export const TypeScriptPackages = {
8
- tsc: 'tsc',
9
- nativePreview: '@typescript/native-preview',
10
- nativePreviewPinned: '@typescript/native-preview@7.0.0-dev.20260221.1',
11
- } as const
12
-
13
- type TypeScriptRunner = Readonly<{
14
- package: (typeof TypeScriptPackages)[keyof typeof TypeScriptPackages]
15
- bin: 'tsc' | 'tsgo'
16
- }>
17
-
18
- export const TYPESCRIPT_RUNNER: TypeScriptRunner = {
19
- package: TypeScriptPackages.nativePreviewPinned,
20
- bin: 'tsgo',
21
- }
@@ -1,36 +0,0 @@
1
- /**
2
- * Worker Naming Constants
3
- *
4
- * Conventions for naming Cloudflare workers to avoid collisions
5
- * between staging and production deployments.
6
- */
7
-
8
- /**
9
- * Worker naming patterns
10
- * - Production workers: {slug} → accessible at {slug}.playcademy.gg
11
- * - Staging workers: staging-{slug} → accessible at {slug}-staging.playcademy.gg
12
- *
13
- * Note: Worker names use a PREFIX (staging-) while hostnames use a SUFFIX (-staging)
14
- * to follow subdomain naming conventions.
15
- */
16
- export const WORKER_NAMING = {
17
- /** Prefix for staging worker names (e.g., "staging-bamboo") */
18
- STAGING_PREFIX: 'staging-',
19
- /** Suffix for staging worker hostnames (e.g., "bamboo-staging.playcademy.gg") */
20
- STAGING_SUFFIX: '-staging',
21
- } as const
22
-
23
- /**
24
- * Prefix for user-defined secrets in Cloudflare Worker bindings.
25
- *
26
- * Cloudflare bindings are flat, so we namespace user secrets with this prefix
27
- * to distinguish them from platform bindings (like GAME_ID, PLAYCADEMY_API_KEY).
28
- *
29
- * Usage:
30
- * - When setting secrets: `bindings[SECRETS_PREFIX + key] = value`
31
- * - When reading secrets: strip prefix and expose as `c.env.secrets.KEY`
32
- *
33
- * @see reconstructSecrets in edge-play/entry/setup.ts
34
- * @see prefixSecrets in edge-play/entry/setup.ts
35
- */
36
- export const SECRETS_PREFIX = 'secrets_'
@@ -1,27 +0,0 @@
1
- /**
2
- * Constants for game backend workers
3
- */
4
-
5
- import { TIMEBACK_ROUTES, WORKER_ENV_VARS, WORKER_NAMING } from '@playcademy/constants'
6
-
7
- /**
8
- * Re-export shared constants from @playcademy/constants
9
- */
10
- export { WORKER_ENV_VARS as ENV_VARS, WORKER_NAMING }
11
-
12
- /**
13
- * Built-in API routes
14
- */
15
- export const ROUTES = {
16
- /** Route index (lists available routes) */
17
- INDEX: '/api',
18
-
19
- /** Health check endpoint */
20
- HEALTH: '/api/health',
21
-
22
- /** TimeBack integration routes */
23
- TIMEBACK: {
24
- END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}`,
25
- GET_XP: `/api${TIMEBACK_ROUTES.GET_XP}`,
26
- },
27
- } as const
@@ -1,247 +0,0 @@
1
- /**
2
- * Middleware Configuration
3
- *
4
- * All middleware for the game backend worker
5
- */
6
-
7
- import { cors } from 'hono/cors'
8
-
9
- import { PlaycademyClient, verifyGameToken } from '@playcademy/sdk/server'
10
-
11
- import { resolveRawSelfWorker, wrapSelfWorkerWithPathResolution } from '../lib/self-dispatch'
12
- import { populateProcessEnv, reconstructSecrets } from './setup'
13
-
14
- import type { Context, Hono } from 'hono'
15
-
16
- import type { AssetsFetcher, HonoEnv } from '../types'
17
- import type { RuntimeConfig } from './types'
18
-
19
- /**
20
- * Register CORS middleware
21
- *
22
- * Allows cross-origin requests with credentials support.
23
- * This is required for authentication systems like Better Auth that use cookies.
24
- *
25
- * TODO: Harden CORS in production - restrict to trusted origins:
26
- * - Game's deploymentUrl (for hosted games)
27
- * - Game's externalUrl (for external games)
28
- * - Platform domains (hub.playcademy.com, hub.dev.playcademy.net)
29
- */
30
- export function registerCors(app: Hono<HonoEnv>): void {
31
- app.use(
32
- '*',
33
- cors({
34
- origin: origin => origin, // Echo back the origin (permissive but allows credentials)
35
- credentials: true, // Required for cookies/sessions (e.g., Better Auth)
36
- allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
37
- allowHeaders: ['Content-Type', 'Authorization'],
38
- // exposeHeaders: ['set-auth-token'],
39
- }),
40
- )
41
- }
42
-
43
- /**
44
- * Register environment setup middleware
45
- *
46
- * Populates process.env, reconstructs secrets, and sets config
47
- */
48
- export function registerEnvSetup(app: Hono<HonoEnv>, config: RuntimeConfig): void {
49
- app.use('*', async (c, next) => {
50
- c.env = { ...c.env }
51
- populateProcessEnv(c.env)
52
- c.env.secrets = reconstructSecrets(c.env)
53
- c.env.SELF = wrapSelfWorkerWithPathResolution(resolveRawSelfWorker(c.env), c.req.url)
54
- c.set('config', config)
55
- c.set('routeMetadata', config.__routeMetadata || [])
56
- await next()
57
- })
58
- }
59
-
60
- /**
61
- * Register SDK initialization middleware
62
- *
63
- * Lazily initializes the Playcademy SDK on first request and
64
- * makes it available via c.get('sdk')
65
- */
66
- export function registerSdkInit(app: Hono<HonoEnv>, config: RuntimeConfig): void {
67
- let sdkPromise: Promise<PlaycademyClient> | null = null
68
-
69
- app.use('*', async (c, next) => {
70
- if (!sdkPromise) {
71
- sdkPromise = PlaycademyClient.init({
72
- apiKey: c.env.PLAYCADEMY_API_KEY,
73
- gameId: c.env.GAME_ID,
74
- baseUrl: c.env.PLAYCADEMY_BASE_URL,
75
- config,
76
- })
77
- }
78
-
79
- c.set('sdk', await sdkPromise)
80
- await next()
81
- })
82
- }
83
-
84
- /**
85
- * Register Playcademy user middleware
86
- *
87
- * Verifies platform game tokens and populates c.get('playcademyUser')
88
- * if a valid token is present in the Authorization header
89
- */
90
- export function registerPlaycademyUser(app: Hono<HonoEnv>): void {
91
- app.use('*', async (c, next) => {
92
- const authHeader = c.req.header('Authorization')
93
-
94
- if (authHeader?.startsWith('Bearer ')) {
95
- const token = authHeader.slice(7)
96
-
97
- try {
98
- const result = await verifyGameToken(token)
99
-
100
- c.set('playcademyUser', result.user)
101
- } catch {
102
- // Invalid/expired token - that's fine, just don't set user
103
- // Routes can decide if they require authentication
104
- }
105
- }
106
-
107
- await next()
108
- })
109
- }
110
-
111
- /**
112
- * Register API 404 handler
113
- *
114
- * Returns a helpful JSON response for unmatched API routes.
115
- * Should be registered after all API routes but before asset fallback.
116
- */
117
- export function registerApiNotFoundHandler(app: Hono<HonoEnv>): void {
118
- app.all('/api/*', async c =>
119
- c.json(
120
- {
121
- error: 'Not Found',
122
- message: `Route ${c.req.method} ${c.req.path} not found`,
123
- path: c.req.path,
124
- method: c.req.method,
125
- },
126
- 404,
127
- ),
128
- )
129
- }
130
-
131
- /**
132
- * Detect if ASSETS binding is an R2 bucket or Workers Assets Fetcher
133
- * Returns true for R2Bucket, false for AssetsFetcher
134
- */
135
- function isR2AssetsBinding(assets: AssetsFetcher | R2Bucket): assets is R2Bucket {
136
- // R2Bucket has .get() method, Fetcher has .fetch() method
137
- return 'get' in assets && typeof assets.get === 'function' && !('fetch' in assets)
138
- }
139
-
140
- /**
141
- * Serve assets from Workers Assets binding (Fetcher)
142
- * Default strategy for games with files <25MB
143
- */
144
- async function serveFromWorkersAssets(
145
- c: Context<HonoEnv>,
146
- assets: AssetsFetcher,
147
- ): Promise<Response> {
148
- // Try to fetch the requested asset
149
- const response = await assets.fetch(c.req.raw)
150
-
151
- // If found, return it
152
- if (response.status !== 404) {
153
- return response
154
- }
155
-
156
- // If not found and it's a file request (has extension), return 404
157
- const path = new URL(c.req.url).pathname
158
-
159
- if (path.includes('.')) {
160
- return response
161
- }
162
-
163
- // Otherwise, fall back to index.html for SPA routing
164
- const indexUrl = new URL(c.req.url)
165
-
166
- indexUrl.pathname = '/index.html'
167
-
168
- return await assets.fetch(new Request(indexUrl.toString()))
169
- }
170
-
171
- /**
172
- * Serve assets from R2 bucket binding
173
- * Fallback strategy for games with large files (≥25MB) like Godot WASM
174
- */
175
- async function serveFromR2(c: Context<HonoEnv>, bucket: R2Bucket): Promise<Response> {
176
- const path = new URL(c.req.url).pathname
177
- const key = path === '/' ? 'index.html' : path.slice(1)
178
-
179
- // Try to fetch the requested asset
180
- const object = await bucket.get(key)
181
-
182
- if (object) {
183
- return new Response(object.body, {
184
- headers: {
185
- 'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream',
186
- // Use 1-hour caching - balances performance with update propagation
187
- // (Godot and other engines use non-hashed filenames)
188
- 'Cache-Control': key === 'index.html' ? 'no-cache' : 'public, max-age=3600',
189
- ETag: object.etag,
190
- },
191
- })
192
- }
193
-
194
- // If not found and it's a file request (has extension), return 404
195
- if (path.includes('.')) {
196
- return c.json({ error: 'Not Found', message: 'Asset not found', path }, 404)
197
- }
198
-
199
- // Otherwise, fall back to index.html for SPA routing
200
- const indexObject = await bucket.get('index.html')
201
-
202
- if (indexObject) {
203
- return new Response(indexObject.body, {
204
- headers: {
205
- 'Content-Type': 'text/html',
206
- 'Cache-Control': 'no-cache',
207
- },
208
- })
209
- }
210
-
211
- return c.json({ error: 'Not Found', message: 'Asset not found', path }, 404)
212
- }
213
-
214
- /**
215
- * Register asset fallback handler
216
- *
217
- * Serves static assets from either Workers Assets or R2 bucket binding.
218
- * Automatically detects which binding type is present and routes accordingly.
219
- * MUST be registered last as it's a catch-all for non-API routes.
220
- *
221
- * Supports two deployment strategies:
222
- * - Workers Assets (default): For games with files <25MB
223
- * - R2 Bucket (fallback): For games with large files like Godot WASM
224
- *
225
- * SPA Routing Support:
226
- * - First tries to fetch the requested path (e.g., /assets/main.js)
227
- * - If 404 and NOT an API route, falls back to /index.html
228
- * - This allows client-side routing (React Router) to work on refresh
229
- */
230
- export function registerAssetFallback(app: Hono<HonoEnv>): void {
231
- app.get('*', async c => {
232
- if (!c.env.ASSETS) {
233
- return c.json(
234
- {
235
- error: 'Not Found',
236
- message: 'Asset not found',
237
- path: c.req.path,
238
- },
239
- 404,
240
- )
241
- }
242
-
243
- return isR2AssetsBinding(c.env.ASSETS)
244
- ? await serveFromR2(c, c.env.ASSETS)
245
- : await serveFromWorkersAssets(c, c.env.ASSETS)
246
- })
247
- }