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,3 +0,0 @@
1
- export * from './errors'
2
- export * from './self-dispatch'
3
- export * from './validation'
@@ -1,244 +0,0 @@
1
- import { beforeEach, describe, expect, it, mock } from 'bun:test'
2
-
3
- import {
4
- normalizeSelfFetchInput,
5
- resolveRawSelfWorker,
6
- wrapSelfWorkerWithPathResolution,
7
- } from './self-dispatch'
8
-
9
- import type { HonoEnv, SelfWorker } from '../types'
10
-
11
- describe('normalizeSelfFetchInput', () => {
12
- const requestUrl = 'https://vocabulon-staging.playcademy.gg/api/admin/genai/sprint'
13
-
14
- it('resolves root-relative paths to the current origin', () => {
15
- const input = normalizeSelfFetchInput('/api/internal/process-batch', requestUrl)
16
-
17
- expect(input).toBeInstanceOf(URL)
18
- expect((input as URL).toString()).toBe(
19
- 'https://vocabulon-staging.playcademy.gg/api/internal/process-batch',
20
- )
21
- })
22
-
23
- it('treats bare paths as root-relative', () => {
24
- const input = normalizeSelfFetchInput('api/internal/process-batch', requestUrl)
25
-
26
- expect(input).toBeInstanceOf(URL)
27
- expect((input as URL).toString()).toBe(
28
- 'https://vocabulon-staging.playcademy.gg/api/internal/process-batch',
29
- )
30
- })
31
-
32
- it('preserves absolute URL strings', () => {
33
- const input = normalizeSelfFetchInput(
34
- 'https://vocabulon-staging.playcademy.gg/api/internal/process-batch',
35
- requestUrl,
36
- )
37
-
38
- expect(input).toBeInstanceOf(URL)
39
- expect((input as URL).toString()).toBe(
40
- 'https://vocabulon-staging.playcademy.gg/api/internal/process-batch',
41
- )
42
- })
43
-
44
- it('preserves query and hash on normalized paths', () => {
45
- const input = normalizeSelfFetchInput('/api/internal/process-batch?attempt=2#step', requestUrl)
46
-
47
- expect(input).toBeInstanceOf(URL)
48
- expect((input as URL).toString()).toBe(
49
- 'https://vocabulon-staging.playcademy.gg/api/internal/process-batch?attempt=2#step',
50
- )
51
- })
52
-
53
- it('passes through URL objects unchanged', () => {
54
- const url = new URL('https://vocabulon-staging.playcademy.gg/api/internal/process-batch')
55
- const input = normalizeSelfFetchInput(url, requestUrl)
56
-
57
- expect(input).toBe(url)
58
- })
59
-
60
- it('supports non-http absolute schemes', () => {
61
- const input = normalizeSelfFetchInput('mailto:test@example.com', requestUrl)
62
-
63
- expect(input).toBeInstanceOf(URL)
64
- expect((input as URL).toString()).toBe('mailto:test@example.com')
65
- })
66
-
67
- it('does not allow protocol-relative input to escape origin', () => {
68
- const input = normalizeSelfFetchInput('//evil.example.com/path', requestUrl)
69
-
70
- expect(input).toBeInstanceOf(URL)
71
- expect((input as URL).toString()).toBe('https://vocabulon-staging.playcademy.gg/evil.example.com/path')
72
- })
73
-
74
- it('passes through Request objects unchanged', () => {
75
- const request = new Request('https://vocabulon-staging.playcademy.gg/api/internal/process-batch')
76
- const input = normalizeSelfFetchInput(request, requestUrl)
77
-
78
- expect(input).toBe(request)
79
- })
80
- })
81
-
82
- describe('wrapSelfWorkerWithPathResolution', () => {
83
- const requestUrl = 'https://vocabulon-staging.playcademy.gg/api/admin/genai/sprint'
84
-
85
- it('normalizes string path inputs before delegating to raw self worker', async () => {
86
- const calls: { input: RequestInfo | URL; init?: RequestInit }[] = []
87
- const raw: SelfWorker = {
88
- fetch: (input, init) => {
89
- calls.push({ input, init })
90
-
91
- return Promise.resolve(new Response(null, { status: 202 }))
92
- },
93
- }
94
-
95
- const self = wrapSelfWorkerWithPathResolution(raw, requestUrl)
96
- const init = { method: 'POST' }
97
- const response = await self.fetch('api/internal/process-batch', init)
98
-
99
- expect(response.status).toBe(202)
100
- expect(calls).toHaveLength(1)
101
- expect(calls[0]?.input).toBeInstanceOf(URL)
102
- expect((calls[0]!.input as URL).toString()).toBe(
103
- 'https://vocabulon-staging.playcademy.gg/api/internal/process-batch',
104
- )
105
- expect(calls[0]?.init).toEqual(init)
106
- })
107
-
108
- it('passes Request objects through to the raw self worker', async () => {
109
- const calls: { input: RequestInfo | URL; init?: RequestInit }[] = []
110
- const raw: SelfWorker = {
111
- fetch: (input, init) => {
112
- calls.push({ input, init })
113
-
114
- return Promise.resolve(new Response(null, { status: 204 }))
115
- },
116
- }
117
- const self = wrapSelfWorkerWithPathResolution(raw, requestUrl)
118
- const req = new Request('https://vocabulon-staging.playcademy.gg/api/internal/process-batch')
119
-
120
- await self.fetch(req)
121
-
122
- expect(calls).toHaveLength(1)
123
- expect(calls[0]?.input).toBe(req)
124
- expect(calls[0]?.init).toBeUndefined()
125
- })
126
- })
127
-
128
- describe('concurrent request isolation (spread-then-mutate)', () => {
129
- it('two spread copies resolve SELF paths using their own request URL', async () => {
130
- const calls: (RequestInfo | URL)[] = []
131
- const raw: SelfWorker = {
132
- fetch: (input: RequestInfo | URL) => {
133
- calls.push(input)
134
-
135
- return Promise.resolve(new Response('ok', { status: 200 }))
136
- },
137
- }
138
-
139
- const sharedEnv = {
140
- __PLAYCADEMY_DISPATCH: { get: (_name: string) => raw },
141
- __PLAYCADEMY_WORKER_NAME: 'staging-vocabulon',
142
- } as unknown as HonoEnv['Bindings']
143
-
144
- const env1 = { ...sharedEnv } as HonoEnv['Bindings']
145
-
146
- env1.SELF = wrapSelfWorkerWithPathResolution(resolveRawSelfWorker(env1), 'https://one.example.com/api/a')
147
-
148
- const env2 = { ...sharedEnv } as HonoEnv['Bindings']
149
-
150
- env2.SELF = wrapSelfWorkerWithPathResolution(resolveRawSelfWorker(env2), 'https://two.example.com/api/b')
151
-
152
- await env1.SELF!.fetch('api/test/ping')
153
- await env2.SELF!.fetch('api/test/ping')
154
- await env1.SELF!.fetch('api/test/ping')
155
-
156
- expect(sharedEnv.SELF).toBeUndefined()
157
- expect(calls).toHaveLength(3)
158
- expect((calls[0] as URL).toString()).toBe('https://one.example.com/api/test/ping')
159
- expect((calls[1] as URL).toString()).toBe('https://two.example.com/api/test/ping')
160
- expect((calls[2] as URL).toString()).toBe('https://one.example.com/api/test/ping')
161
- })
162
- })
163
-
164
- describe('resolveRawSelfWorker', () => {
165
- beforeEach(() => {
166
- mock.restore()
167
- })
168
-
169
- it('uses dispatch namespace binding when available', async () => {
170
- const targetWorker: SelfWorker = {
171
- fetch: () => Promise.resolve(new Response('ok')),
172
- }
173
-
174
- const env = {
175
- __PLAYCADEMY_DISPATCH: {
176
- get: (name: string) => {
177
- expect(name).toBe('staging-vocabulon')
178
-
179
- return targetWorker
180
- },
181
- },
182
- __PLAYCADEMY_WORKER_NAME: 'staging-vocabulon',
183
- } as unknown as HonoEnv['Bindings']
184
-
185
- const resolved = resolveRawSelfWorker(env)
186
- const response = await resolved.fetch('https://vocabulon-staging.playcademy.gg/api/internal/process')
187
-
188
- expect(response.status).toBe(200)
189
- })
190
-
191
- it('falls back to global fetch when dispatch binding is unavailable', async () => {
192
- const fetchMock = mock(() => Promise.resolve(new Response('fallback', { status: 204 })))
193
- const originalFetch = globalThis.fetch
194
-
195
- globalThis.fetch = fetchMock as unknown as typeof fetch
196
-
197
- try {
198
- const env = {} as HonoEnv['Bindings']
199
- const resolved = resolveRawSelfWorker(env)
200
- const response = await resolved.fetch(
201
- 'https://vocabulon-staging.playcademy.gg/api/internal/process',
202
- )
203
-
204
- expect(response.status).toBe(204)
205
- expect(fetchMock).toHaveBeenCalledTimes(1)
206
-
207
- const firstCall = fetchMock.mock.calls[0] as unknown[] | undefined
208
-
209
- expect(firstCall).toBeDefined()
210
-
211
- const arg = firstCall?.[0]
212
-
213
- expect(arg).toBeInstanceOf(Request)
214
- } finally {
215
- globalThis.fetch = originalFetch
216
- }
217
- })
218
-
219
- it('falls back to global fetch when worker name is missing', async () => {
220
- const fetchMock = mock(() => Promise.resolve(new Response('fallback', { status: 206 })))
221
- const originalFetch = globalThis.fetch
222
-
223
- globalThis.fetch = fetchMock as unknown as typeof fetch
224
-
225
- try {
226
- const env = {
227
- __PLAYCADEMY_DISPATCH: {
228
- get: () => {
229
- throw new Error('should not be called when worker name is missing')
230
- },
231
- },
232
- } as unknown as HonoEnv['Bindings']
233
- const resolved = resolveRawSelfWorker(env)
234
- const response = await resolved.fetch(
235
- 'https://vocabulon-staging.playcademy.gg/api/internal/process',
236
- )
237
-
238
- expect(response.status).toBe(206)
239
- expect(fetchMock).toHaveBeenCalledTimes(1)
240
- } finally {
241
- globalThis.fetch = originalFetch
242
- }
243
- })
244
- })
@@ -1,41 +0,0 @@
1
- import type { HonoEnv, SelfWorker } from '../types'
2
-
3
- function isAbsoluteUrl(value: string): boolean {
4
- return /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(value)
5
- }
6
-
7
- export function normalizeSelfFetchInput(
8
- input: RequestInfo | URL,
9
- requestUrl: string,
10
- ): RequestInfo | URL {
11
- if (typeof input !== 'string') {
12
- return input
13
- }
14
-
15
- if (isAbsoluteUrl(input)) {
16
- return new URL(input)
17
- }
18
-
19
- const origin = new URL(requestUrl).origin
20
- const sanitized = input.startsWith('//') ? `/${input.replace(/^\/+/, '')}` : input
21
- const pathname = sanitized.startsWith('/') ? sanitized : `/${sanitized}`
22
-
23
- return new URL(pathname, origin)
24
- }
25
-
26
- export function wrapSelfWorkerWithPathResolution(raw: SelfWorker, requestUrl: string): SelfWorker {
27
- return {
28
- fetch: (input, init) => raw.fetch(normalizeSelfFetchInput(input, requestUrl), init),
29
- }
30
- }
31
-
32
- export function resolveRawSelfWorker(env: HonoEnv['Bindings']): SelfWorker {
33
- const dispatchBinding = env.__PLAYCADEMY_DISPATCH
34
- const workerName = env.__PLAYCADEMY_WORKER_NAME
35
-
36
- if (dispatchBinding && workerName) {
37
- return dispatchBinding.get(workerName)
38
- }
39
-
40
- return { fetch: (input, init) => fetch(new Request(input, init)) }
41
- }
@@ -1,190 +0,0 @@
1
- import { describe, expect, it } from 'bun:test'
2
-
3
- import {
4
- isValidGrade,
5
- isValidSubject,
6
- validateCourseConfig,
7
- VALID_GRADES,
8
- VALID_SUBJECTS,
9
- } from './validation'
10
-
11
- import type { PlaycademyConfig } from '@playcademy/sdk/server'
12
-
13
- describe('isValidGrade', () => {
14
- it('accepts valid grade levels', () => {
15
- for (const grade of VALID_GRADES) {
16
- expect(isValidGrade(grade)).toBe(true)
17
- }
18
- })
19
-
20
- it('accepts Pre-K (-1), Kindergarten (0), and AP (13)', () => {
21
- expect(isValidGrade(-1)).toBe(true) // Pre-K
22
- expect(isValidGrade(0)).toBe(true) // Kindergarten
23
- expect(isValidGrade(13)).toBe(true) // AP
24
- })
25
-
26
- it('rejects invalid grade levels', () => {
27
- expect(isValidGrade(-2)).toBe(false)
28
- expect(isValidGrade(14)).toBe(false)
29
- expect(isValidGrade(100)).toBe(false)
30
- })
31
-
32
- it('rejects non-integer values', () => {
33
- expect(isValidGrade(3.5)).toBe(false)
34
- expect(isValidGrade(NaN)).toBe(false)
35
- expect(isValidGrade(Infinity)).toBe(false)
36
- })
37
-
38
- it('rejects non-number values', () => {
39
- expect(isValidGrade('3')).toBe(false)
40
- expect(isValidGrade(null)).toBe(false)
41
- expect(isValidGrade(undefined)).toBe(false)
42
- expect(isValidGrade({})).toBe(false)
43
- })
44
- })
45
-
46
- describe('isValidSubject', () => {
47
- it('accepts valid subjects', () => {
48
- for (const subject of VALID_SUBJECTS) {
49
- expect(isValidSubject(subject)).toBe(true)
50
- }
51
- })
52
-
53
- it('accepts common subjects', () => {
54
- expect(isValidSubject('Math')).toBe(true)
55
- expect(isValidSubject('FastMath')).toBe(true)
56
- expect(isValidSubject('Reading')).toBe(true)
57
- expect(isValidSubject('Science')).toBe(true)
58
- })
59
-
60
- it('rejects invalid subjects', () => {
61
- expect(isValidSubject('History')).toBe(false)
62
- expect(isValidSubject('Art')).toBe(false)
63
- expect(isValidSubject('math')).toBe(false) // case-sensitive
64
- expect(isValidSubject('MATH')).toBe(false)
65
- })
66
-
67
- it('rejects non-string values', () => {
68
- expect(isValidSubject(123)).toBe(false)
69
- expect(isValidSubject(null)).toBe(false)
70
- expect(isValidSubject(undefined)).toBe(false)
71
- expect(isValidSubject({})).toBe(false)
72
- })
73
- })
74
-
75
- describe('validateCourseConfig', () => {
76
- /**
77
- * Simulates a typical playcademy.config.js for a math game
78
- * covering grades 3-5.
79
- */
80
- const mathGameConfig: PlaycademyConfig = {
81
- name: 'Math Adventure',
82
- integrations: {
83
- timeback: {
84
- courses: [
85
- { subject: 'Math', grade: 3, totalXp: 1000, masterableUnits: 10 },
86
- { subject: 'Math', grade: 4, totalXp: 1200, masterableUnits: 12 },
87
- { subject: 'Math', grade: 5, totalXp: 1500, masterableUnits: 15 },
88
- ],
89
- },
90
- },
91
- }
92
-
93
- /**
94
- * Simulates a multi-subject game config.
95
- */
96
- const multiSubjectConfig: PlaycademyConfig = {
97
- name: 'Learning Hub',
98
- integrations: {
99
- timeback: {
100
- courses: [
101
- { subject: 'Math', grade: 3, totalXp: 500, masterableUnits: 5 },
102
- { subject: 'Reading', grade: 3, totalXp: 500, masterableUnits: 5 },
103
- { subject: 'Science', grade: 4, totalXp: 600, masterableUnits: 6 },
104
- ],
105
- },
106
- },
107
- }
108
-
109
- it('returns null for configured grade/subject combinations', () => {
110
- expect(validateCourseConfig({ grade: 3, subject: 'Math', config: mathGameConfig })).toBeNull()
111
- expect(validateCourseConfig({ grade: 4, subject: 'Math', config: mathGameConfig })).toBeNull()
112
- expect(validateCourseConfig({ grade: 5, subject: 'Math', config: mathGameConfig })).toBeNull()
113
- })
114
-
115
- it('returns error for unconfigured grade', () => {
116
- const result = validateCourseConfig({ grade: 6, subject: 'Math', config: mathGameConfig })
117
-
118
- expect(result).not.toBeNull()
119
- expect(result?.error).toContain('Invalid grade/subject combination')
120
- expect(result?.error).toContain('Math (Grade 6)')
121
- expect(result?.error).toContain('Configured courses:')
122
- })
123
-
124
- it('returns error for unconfigured subject', () => {
125
- const result = validateCourseConfig({ grade: 3, subject: 'Reading', config: mathGameConfig })
126
-
127
- expect(result).not.toBeNull()
128
- expect(result?.error).toContain('Invalid grade/subject combination')
129
- expect(result?.error).toContain('Reading (Grade 3)')
130
- })
131
-
132
- it('lists all configured courses in error message', () => {
133
- const result = validateCourseConfig({ grade: 1, subject: 'Math', config: mathGameConfig })
134
-
135
- expect(result?.error).toContain('Math (Grade 3)')
136
- expect(result?.error).toContain('Math (Grade 4)')
137
- expect(result?.error).toContain('Math (Grade 5)')
138
- })
139
-
140
- it('validates multi-subject configs correctly', () => {
141
- // Configured combinations
142
- expect(
143
- validateCourseConfig({ grade: 3, subject: 'Math', config: multiSubjectConfig }),
144
- ).toBeNull()
145
- expect(
146
- validateCourseConfig({ grade: 3, subject: 'Reading', config: multiSubjectConfig }),
147
- ).toBeNull()
148
- expect(
149
- validateCourseConfig({ grade: 4, subject: 'Science', config: multiSubjectConfig }),
150
- ).toBeNull()
151
-
152
- // Not configured: Math grade 4 (only Science is grade 4)
153
- const result = validateCourseConfig({
154
- grade: 4,
155
- subject: 'Math',
156
- config: multiSubjectConfig,
157
- })
158
-
159
- expect(result).not.toBeNull()
160
- expect(result?.error).toContain('Math (Grade 4)')
161
- })
162
-
163
- it('handles config with no courses', () => {
164
- const emptyConfig: PlaycademyConfig = {
165
- name: 'Empty Game',
166
- integrations: {
167
- timeback: {
168
- courses: [],
169
- },
170
- },
171
- }
172
-
173
- const result = validateCourseConfig({ grade: 3, subject: 'Math', config: emptyConfig })
174
-
175
- expect(result).not.toBeNull()
176
- expect(result?.error).toContain('Configured courses: none')
177
- })
178
-
179
- it('handles config with no timeback integration', () => {
180
- const noTimebackConfig: PlaycademyConfig = {
181
- name: 'No Timeback',
182
- integrations: {},
183
- }
184
-
185
- const result = validateCourseConfig({ grade: 3, subject: 'Math', config: noTimebackConfig })
186
-
187
- expect(result).not.toBeNull()
188
- expect(result?.error).toContain('Configured courses: none')
189
- })
190
- })
@@ -1,64 +0,0 @@
1
- /**
2
- * Shared validation utilities for edge-play routes.
3
- */
4
-
5
- import type { PlaycademyConfig } from '@playcademy/sdk/server'
6
-
7
- /** Valid grade levels: -1 (Pre-K), 0 (Kindergarten), 1-12 (Grades), 13 (AP) */
8
- export const VALID_GRADES = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13] as const
9
-
10
- /** Valid TimeBack subject values */
11
- export const VALID_SUBJECTS = [
12
- 'Reading',
13
- 'Language',
14
- 'Vocabulary',
15
- 'Social Studies',
16
- 'Writing',
17
- 'Science',
18
- 'FastMath',
19
- 'Math',
20
- 'None',
21
- ] as const
22
-
23
- export type ValidGrade = (typeof VALID_GRADES)[number]
24
- export type ValidSubject = (typeof VALID_SUBJECTS)[number]
25
-
26
- export function isValidGrade(value: unknown): value is ValidGrade {
27
- return (
28
- typeof value === 'number' &&
29
- Number.isInteger(value) &&
30
- VALID_GRADES.includes(value as ValidGrade)
31
- )
32
- }
33
-
34
- export function isValidSubject(value: unknown): value is ValidSubject {
35
- return typeof value === 'string' && VALID_SUBJECTS.includes(value as ValidSubject)
36
- }
37
-
38
- /**
39
- * Validate that a grade/subject combination is configured for this game.
40
- * Returns an error object if invalid, null if valid.
41
- */
42
- export function validateCourseConfig(params: {
43
- grade: number
44
- subject: string
45
- config: PlaycademyConfig
46
- }): { error: string } | null {
47
- const { grade, subject, config } = params
48
- const timebackConfig = config.integrations?.timeback
49
- const configuredCourse = timebackConfig?.courses?.find(
50
- course => course.grade === grade && course.subject === subject,
51
- )
52
-
53
- if (!configuredCourse) {
54
- const configured = timebackConfig?.courses
55
- ?.map(c => `${c.subject} (Grade ${c.grade})`)
56
- .join(', ')
57
-
58
- return {
59
- error: `Invalid grade/subject combination: ${subject} (Grade ${grade}). Configured courses: ${configured || 'none'}`,
60
- }
61
- }
62
-
63
- return null
64
- }
@@ -1,54 +0,0 @@
1
- /**
2
- * Polyfills for Node.js modules in edge runtimes
3
- *
4
- * Provides minimal implementations to allow bundling.
5
- * These should NOT be called at runtime (config is provided directly).
6
- */
7
-
8
- const notAvailable = name => {
9
- throw new Error(`${name} not available in Worker runtime - config should be provided directly`)
10
- }
11
-
12
- // fs exports
13
- export const existsSync = () => notAvailable('fs.existsSync')
14
- export const readdirSync = () => notAvailable('fs.readdirSync')
15
- export const statSync = () => notAvailable('fs.statSync')
16
- export const readFile = () => notAvailable('fs/promises.readFile')
17
-
18
- // path exports
19
- export const resolve = (...args) => args.join('/')
20
- export const dirname = p => p.split('/').slice(0, -1).join('/')
21
- export const parse = p => ({
22
- dir: dirname(p),
23
- name: p.split('/').pop(),
24
- base: p.split('/').pop(),
25
- ext: '',
26
- root: '/',
27
- })
28
- export const join = (...args) => args.join('/')
29
-
30
- // os exports
31
- export const tmpdir = () => '/tmp'
32
- export const homedir = () => notAvailable('os.homedir')
33
-
34
- // Default export for 'process' module
35
- export default {
36
- env: {
37
- // These will be set by the Worker environment
38
- // Cloudflare Workers uses env bindings, but we access via process.env
39
- PLAYCADEMY_API_KEY: globalThis.PLAYCADEMY_API_KEY || '',
40
- GAME_ID: globalThis.GAME_ID || '',
41
- PLAYCADEMY_BASE_URL: globalThis.PLAYCADEMY_BASE_URL || '',
42
- },
43
- cwd: () => '/',
44
- }
45
-
46
- // Named export as well for compatibility
47
- export const process = {
48
- env: {
49
- PLAYCADEMY_API_KEY: globalThis.PLAYCADEMY_API_KEY || '',
50
- GAME_ID: globalThis.GAME_ID || '',
51
- PLAYCADEMY_BASE_URL: globalThis.PLAYCADEMY_BASE_URL || '',
52
- },
53
- cwd: () => '/',
54
- }
@@ -1,59 +0,0 @@
1
- /**
2
- * Route Registration Helper
3
- *
4
- * Conditionally registers built-in integration routes based on enabled integrations.
5
- * Uses dynamic imports to enable tree-shaking (unused integrations are removed from bundle).
6
- */
7
-
8
- import { ROUTES } from './constants'
9
-
10
- import type { Hono } from 'hono'
11
-
12
- import type { HonoEnv, Integrations } from './types'
13
-
14
- /**
15
- * Registers all enabled built-in integration routes
16
- *
17
- * @param app - Hono application instance
18
- * @param integrations - Enabled integrations from config
19
- */
20
- export async function registerBuiltinRoutes(app: Hono<HonoEnv>, integrations?: Integrations) {
21
- // Route discovery (always included)
22
- const routesIndex = await import('./routes/index')
23
-
24
- app.get(ROUTES.INDEX, routesIndex.GET)
25
-
26
- // Health check (always included)
27
- const health = await import('./routes/health')
28
-
29
- app.get(ROUTES.HEALTH, health.GET)
30
-
31
- // TimeBack integration
32
- if (integrations?.timeback) {
33
- const [endActivity, getXp] = await Promise.all([
34
- import('./routes/integrations/timeback/end-activity'),
35
- import('./routes/integrations/timeback/get-xp'),
36
- ])
37
-
38
- app.post(ROUTES.TIMEBACK.END_ACTIVITY, endActivity.POST)
39
- app.get(ROUTES.TIMEBACK.GET_XP, getXp.GET)
40
- } else if (integrations?.timeback === null) {
41
- app.post('/api/integrations/timeback/end-activity', async c =>
42
- c.json({
43
- status: 'ok',
44
- __playcademyDevWarning: 'timeback-not-configured',
45
- }),
46
- )
47
- app.get('/api/integrations/timeback/xp', async c =>
48
- c.json({
49
- timebackId: '',
50
- totalXp: 0,
51
- __playcademyDevWarning: 'timeback-not-configured',
52
- }),
53
- )
54
- }
55
-
56
- // TODO: Auth integration
57
- // TODO: Storage integration
58
- // TODO: Realtime integration
59
- }