playcademy 0.13.18 → 0.13.20

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.
@@ -9,6 +9,33 @@ export { GAME_WORKER_DOMAINS, PLAYCADEMY_BASE_URLS, PLAYCADEMY_DOMAINS } from '@
9
9
  */
10
10
  declare const DEFAULT_API_ROUTES_DIRECTORY = "server/api";
11
11
 
12
+ /**
13
+ * Configuration file constants
14
+ *
15
+ * Constants for user configuration files (tsconfig, .env, playcademy.config.js, etc.)
16
+ */
17
+ /**
18
+ * Environment files loaded in order (later files override earlier ones)
19
+ *
20
+ * Load order:
21
+ * 1. .env ← Loaded first (base, can be committed)
22
+ * 2. .env.development ← Overrides .env
23
+ * 3. .env.local ← Overrides all (gitignored, personal secrets)
24
+ *
25
+ * Example: If KEY exists in both .env and .env.local, .env.local wins.
26
+ *
27
+ * This matches the pattern used by Vite, Bun, and other modern tools.
28
+ */
29
+ declare const ENV_FILES: readonly [".env", ".env.development", ".env.local"];
30
+ /**
31
+ * TypeScript config files to check (in priority order)
32
+ *
33
+ * Priority:
34
+ * 1. tsconfig.app.json ← Modern tooling (Vite, etc.) - try first
35
+ * 2. tsconfig.json ← Standard TypeScript config - fallback
36
+ */
37
+ declare const TSCONFIG_FILES: readonly ["tsconfig.app.json", "tsconfig.json"];
38
+
12
39
  /**
13
40
  * Database-related constants
14
41
  */
@@ -99,4 +126,4 @@ declare const CONFIG_FILE_NAMES: string[];
99
126
  */
100
127
  declare const CLOUDFLARE_COMPATIBILITY_DATE = "2024-01-01";
101
128
 
102
- export { CALLBACK_PATH, CALLBACK_PORT, CLI_DEFAULT_OUTPUTS, CLI_DIRECTORIES, CLI_FILES, CLI_USER_DIRECTORIES, CLOUDFLARE_COMPATIBILITY_DATE, CONFIG_FILE_NAMES, DEFAULT_API_ROUTES_DIRECTORY, DEFAULT_DATABASE_DIRECTORY, DEFAULT_PORTS, DEFAULT_SCHEMA_DIRECTORY, DEFAULT_SEED_FILE, SSO_AUTH_TIMEOUT_MS, WORKSPACE_NAME };
129
+ export { CALLBACK_PATH, CALLBACK_PORT, CLI_DEFAULT_OUTPUTS, CLI_DIRECTORIES, CLI_FILES, CLI_USER_DIRECTORIES, CLOUDFLARE_COMPATIBILITY_DATE, CONFIG_FILE_NAMES, DEFAULT_API_ROUTES_DIRECTORY, DEFAULT_DATABASE_DIRECTORY, DEFAULT_PORTS, DEFAULT_SCHEMA_DIRECTORY, DEFAULT_SEED_FILE, ENV_FILES, SSO_AUTH_TIMEOUT_MS, TSCONFIG_FILES, WORKSPACE_NAME };
package/dist/constants.js CHANGED
@@ -1,6 +1,22 @@
1
1
  // src/constants/api.ts
2
2
  var DEFAULT_API_ROUTES_DIRECTORY = "server/api";
3
3
 
4
+ // src/constants/config.ts
5
+ var ENV_FILES = [
6
+ ".env",
7
+ // Loaded first
8
+ ".env.development",
9
+ // Overrides .env
10
+ ".env.local"
11
+ // Overrides all (highest priority)
12
+ ];
13
+ var TSCONFIG_FILES = [
14
+ "tsconfig.app.json",
15
+ // Modern tooling (try first)
16
+ "tsconfig.json"
17
+ // Standard (fallback)
18
+ ];
19
+
4
20
  // src/constants/database.ts
5
21
  var DEFAULT_DATABASE_DIRECTORY = "db";
6
22
  var DEFAULT_SCHEMA_DIRECTORY = "db/schema";
@@ -121,9 +137,11 @@ export {
121
137
  DEFAULT_PORTS,
122
138
  DEFAULT_SCHEMA_DIRECTORY,
123
139
  DEFAULT_SEED_FILE,
140
+ ENV_FILES,
124
141
  GAME_WORKER_DOMAINS,
125
142
  PLAYCADEMY_BASE_URLS,
126
143
  PLAYCADEMY_DOMAINS,
127
144
  SSO_AUTH_TIMEOUT_MS,
145
+ TSCONFIG_FILES,
128
146
  WORKSPACE_NAME
129
147
  };
package/dist/db.js CHANGED
@@ -412,6 +412,7 @@ function getPackageManager() {
412
412
  // src/lib/core/logger.ts
413
413
  import {
414
414
  blue,
415
+ blueBright,
415
416
  bold as bold2,
416
417
  cyan,
417
418
  dim as dim2,
@@ -424,8 +425,10 @@ import {
424
425
  } from "colorette";
425
426
  import { colorize } from "json-colorizer";
426
427
  function customTransform(text) {
427
- const highlightCode = (text2) => text2.replace(/`([^`]+)`/g, (_, code) => greenBright(code));
428
- return highlightCode(text);
428
+ let result = text;
429
+ result = result.replace(/`([^`]+)`/g, (_, code) => greenBright(code));
430
+ result = result.replace(/<([^>]+)>/g, (_, path) => blueBright(path));
431
+ return result;
429
432
  }
430
433
  function formatTable(data, title) {
431
434
  if (data.length === 0) return;
@@ -0,0 +1,74 @@
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 } from '@playcademy/sdk/server'
10
+
11
+ import { populateProcessEnv, reconstructSecrets } from './setup'
12
+
13
+ import type { Hono } from 'hono'
14
+ import type { HonoEnv } from '../types'
15
+ import type { RuntimeConfig } from './types'
16
+
17
+ /**
18
+ * Register CORS middleware
19
+ *
20
+ * TODO: Harden CORS in production - restrict to trusted origins:
21
+ * - Game's assetBundleBase (for hosted games)
22
+ * - Game's externalUrl (for external games)
23
+ * - Platform frontend domains (hub.playcademy.com, hub.dev.playcademy.net)
24
+ * This would require passing game metadata through env bindings during deployment
25
+ */
26
+ export function registerCors(app: Hono<HonoEnv>): void {
27
+ app.use(
28
+ '*',
29
+ cors({
30
+ origin: '*', // Permissive for now
31
+ allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
32
+ allowHeaders: ['Content-Type', 'Authorization'],
33
+ }),
34
+ )
35
+ }
36
+
37
+ /**
38
+ * Register environment setup middleware
39
+ *
40
+ * Populates process.env, reconstructs secrets, and sets config
41
+ */
42
+ export function registerEnvSetup(app: Hono<HonoEnv>, config: RuntimeConfig): void {
43
+ app.use('*', async (c, next) => {
44
+ populateProcessEnv(c.env)
45
+ c.env.secrets = reconstructSecrets(c.env)
46
+ c.set('config', config)
47
+ c.set('routeMetadata', config.__routeMetadata || [])
48
+ await next()
49
+ })
50
+ }
51
+
52
+ /**
53
+ * Register SDK initialization middleware
54
+ *
55
+ * Lazily initializes the Playcademy SDK on first request and
56
+ * makes it available via c.get('sdk')
57
+ */
58
+ export function registerSdkInit(app: Hono<HonoEnv>, config: RuntimeConfig): void {
59
+ let sdkPromise: Promise<PlaycademyClient> | null = null
60
+
61
+ app.use('*', async (c, next) => {
62
+ if (!sdkPromise) {
63
+ sdkPromise = PlaycademyClient.init({
64
+ apiKey: c.env.PLAYCADEMY_API_KEY,
65
+ gameId: c.env.GAME_ID,
66
+ baseUrl: c.env.PLAYCADEMY_BASE_URL,
67
+ config,
68
+ })
69
+ }
70
+
71
+ c.set('sdk', await sdkPromise)
72
+ await next()
73
+ })
74
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Environment Setup Utilities
3
+ *
4
+ * Helper functions for preparing the Worker environment
5
+ */
6
+
7
+ import { ENV_VARS } from '../constants'
8
+
9
+ import type { ServerEnv } from '../types'
10
+
11
+ /**
12
+ * Populate process.env from Worker bindings for SDK compatibility
13
+ */
14
+ export function populateProcessEnv(env: ServerEnv): void {
15
+ globalThis.process.env = {
16
+ [ENV_VARS.PLAYCADEMY_API_KEY]: env.PLAYCADEMY_API_KEY,
17
+ [ENV_VARS.GAME_ID]: env.GAME_ID,
18
+ [ENV_VARS.PLAYCADEMY_BASE_URL]: env.PLAYCADEMY_BASE_URL,
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Reconstruct secrets object from flat Cloudflare bindings
24
+ *
25
+ * Cloudflare bindings are flat (secrets_KEY_NAME), but we expose them
26
+ * as c.env.secrets.KEY_NAME for better developer ergonomics.
27
+ */
28
+ export function reconstructSecrets(env: Record<string, unknown>): Record<string, string> {
29
+ const secrets: Record<string, string> = {}
30
+
31
+ for (const key in env) {
32
+ if (key.startsWith('secrets_')) {
33
+ const secretKey = key.slice(8) // Remove 'secrets_' prefix
34
+ secrets[secretKey] = env[key] as string
35
+ }
36
+ }
37
+
38
+ return secrets
39
+ }
40
+
41
+ /**
42
+ * Setup process global polyfill for SDK compatibility
43
+ *
44
+ * SDK code may reference process.env without importing it
45
+ */
46
+ export function setupProcessGlobal(): void {
47
+ // @ts-expect-error - Adding global for Worker environment
48
+ globalThis.process = {
49
+ env: {}, // Populated per-request from Worker env bindings
50
+ cwd: () => '/',
51
+ }
52
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Entry-specific types
3
+ *
4
+ * Types used only within the entry point bundling and runtime
5
+ */
6
+
7
+ import type { PlaycademyConfig } from '@playcademy/sdk/server'
8
+
9
+ /**
10
+ * Route metadata injected at build time by CLI
11
+ */
12
+ export interface RouteMetadata {
13
+ /** Route path (e.g., '/hello') */
14
+ path: string
15
+ /** Source file path relative to api directory (e.g., 'hello.ts') */
16
+ file: string
17
+ /** HTTP methods supported by this route */
18
+ methods?: string[]
19
+ }
20
+
21
+ /**
22
+ * Runtime configuration with CLI-injected metadata
23
+ *
24
+ * This is the config available at runtime after esbuild's `define` injection.
25
+ * It includes both the user's playcademy.config.js and build-time metadata.
26
+ */
27
+ export interface RuntimeConfig extends PlaycademyConfig {
28
+ /** Route metadata injected by CLI during bundling */
29
+ __routeMetadata?: RouteMetadata[]
30
+ }
@@ -9,14 +9,12 @@
9
9
  */
10
10
 
11
11
  import { Hono } from 'hono'
12
- import { cors } from 'hono/cors'
13
12
 
14
- import { PlaycademyClient } from '@playcademy/sdk/server'
15
-
16
- import { ENV_VARS } from './constants'
13
+ import { registerCors, registerEnvSetup, registerSdkInit } from './entry/middleware'
14
+ import { setupProcessGlobal } from './entry/setup'
17
15
  import { registerBuiltinRoutes } from './register-routes'
18
16
 
19
- import type { PlaycademyConfig } from '@playcademy/sdk/server'
17
+ import type { RuntimeConfig } from './entry/types'
20
18
  import type { HonoEnv } from './types'
21
19
 
22
20
  /**
@@ -33,76 +31,27 @@ import type { HonoEnv } from './types'
33
31
  * This enables tree-shaking: if timeback is not configured, those code paths are removed.
34
32
  * The bundled Worker only includes the routes that are actually enabled.
35
33
  */
36
- declare const PLAYCADEMY_CONFIG: PlaycademyConfig & {
37
- customRoutes?: Array<{ path: string; file: string }>
38
- }
34
+ declare const PLAYCADEMY_CONFIG: RuntimeConfig
39
35
 
40
- // XXX: Polyfill process global for SDK compatibility
41
- // SDK code may reference process.env without importing it
42
- // @ts-expect-error - Adding global for Worker environment
43
- globalThis.process = {
44
- env: {}, // Populated per-request from Worker env bindings
45
- cwd: () => '/',
46
- }
36
+ // Setup process global polyfill for SDK compatibility
37
+ setupProcessGlobal()
47
38
 
39
+ // Create Hono app
48
40
  const app = new Hono<HonoEnv>()
49
41
 
50
- // TODO: Harden CORS in production - restrict to trusted origins:
51
- // - Game's assetBundleBase (for hosted games)
52
- // - Game's externalUrl (for external games)
53
- // - Platform frontend domains (hub.playcademy.com, hub.dev.playcademy.net)
54
- // This would require passing game metadata through env bindings during deployment
55
- app.use(
56
- '*',
57
- cors({
58
- origin: '*', // Permissive for now
59
- allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
60
- allowHeaders: ['Content-Type', 'Authorization'],
61
- }),
62
- )
63
-
64
- let sdkPromise: Promise<PlaycademyClient> | null = null
65
-
66
- app.use('*', async (c, next) => {
67
- // Populate process.env from Worker bindings for SDK compatibility
68
- globalThis.process.env = {
69
- [ENV_VARS.PLAYCADEMY_API_KEY]: c.env.PLAYCADEMY_API_KEY,
70
- [ENV_VARS.GAME_ID]: c.env.GAME_ID,
71
- [ENV_VARS.PLAYCADEMY_BASE_URL]: c.env.PLAYCADEMY_BASE_URL,
72
- }
73
-
74
- // Set config for all routes
75
- c.set('config', PLAYCADEMY_CONFIG)
76
- c.set('customRoutes', PLAYCADEMY_CONFIG.customRoutes || [])
77
-
78
- await next()
79
- })
80
-
81
- // Initialize SDK lazily on first request
82
- app.use('*', async (c, next) => {
83
- if (!sdkPromise) {
84
- sdkPromise = PlaycademyClient.init({
85
- apiKey: c.env[ENV_VARS.PLAYCADEMY_API_KEY],
86
- gameId: c.env[ENV_VARS.GAME_ID],
87
- baseUrl: c.env[ENV_VARS.PLAYCADEMY_BASE_URL],
88
- config: PLAYCADEMY_CONFIG,
89
- })
90
- }
91
-
92
- c.set('sdk', await sdkPromise)
93
- await next()
94
- })
95
-
96
- /**
97
- * Register built-in integration routes based on enabled integrations
98
- *
99
- * This function conditionally imports and registers routes like:
100
- * - POST /api/integrations/timeback/end-activity (if timeback enabled)
101
- * - GET /api/health (always included)
102
- *
103
- * Uses dynamic imports for tree-shaking: if an integration is not enabled,
104
- * its route code is completely removed from the bundle.
105
- */
42
+ // Register middleware
43
+ registerCors(app)
44
+ registerEnvSetup(app, PLAYCADEMY_CONFIG)
45
+ registerSdkInit(app, PLAYCADEMY_CONFIG)
46
+
47
+ // Register built-in integration routes based on enabled integrations
48
+ // This function conditionally imports and registers routes like:
49
+ // - GET /api (always included)
50
+ // - GET /api/health (always included)
51
+ // - POST /api/integrations/timeback/end-activity (if timeback enabled)
52
+ //
53
+ // Uses dynamic imports for tree-shaking: if an integration is not enabled,
54
+ // its route code is completely removed from the bundle.
106
55
  await registerBuiltinRoutes(app, PLAYCADEMY_CONFIG.integrations)
107
56
 
108
57
  export default app
@@ -5,40 +5,69 @@
5
5
  */
6
6
 
7
7
  import type { Context } from 'hono'
8
- import type { HonoEnv } from '../types'
8
+ import type { PlaycademyClient, PlaycademyConfig } from '@playcademy/sdk/server'
9
+ import type { RouteMetadata } from '../entry/types'
10
+ import type { HonoEnv, ServerEnv } from '../types'
11
+
12
+ /**
13
+ * Check if all required environment variables are configured
14
+ */
15
+ function isEnvConfigured(env: ServerEnv): boolean {
16
+ return !!env.PLAYCADEMY_API_KEY && !!env.GAME_ID && !!env.PLAYCADEMY_BASE_URL
17
+ }
18
+
19
+ /**
20
+ * Get count of configured secrets
21
+ */
22
+ function getSecretsCount(env: ServerEnv): number {
23
+ return env.secrets ? Object.keys(env.secrets).length : 0
24
+ }
25
+
26
+ /**
27
+ * Build list of enabled integration names
28
+ */
29
+ function getEnabledIntegrations(config?: PlaycademyConfig): string[] {
30
+ const enabled: string[] = []
31
+ if (config?.integrations?.timeback) enabled.push('timeback')
32
+ if (config?.integrations?.customRoutes) enabled.push('customRoutes')
33
+ if (config?.integrations?.database) enabled.push('database')
34
+ if (config?.integrations?.kv) enabled.push('kv')
35
+ if (config?.integrations?.bucket) enabled.push('bucket')
36
+ return enabled
37
+ }
38
+
39
+ /**
40
+ * Format routes into readable objects with separate path and methods
41
+ */
42
+ function formatRoutes(routes: RouteMetadata[]): Array<{ path: string; methods: string[] }> {
43
+ return routes.map(r => ({
44
+ path: r.path,
45
+ methods: r.methods || ['*'],
46
+ }))
47
+ }
48
+
49
+ /**
50
+ * Get TimeBack debug info if applicable
51
+ */
52
+ function getTimebackDebugInfo(config?: PlaycademyConfig, sdk?: PlaycademyClient) {
53
+ if (config?.integrations?.timeback && sdk?.timeback?.courseId) {
54
+ return { timeback: { courseId: sdk.timeback.courseId } }
55
+ }
56
+ return {}
57
+ }
9
58
 
10
59
  export async function GET(c: Context<HonoEnv>): Promise<Response> {
11
60
  const config = c.get('config')
12
61
  const sdk = c.get('sdk')
62
+ const routeMetadata = c.get('routeMetadata')
13
63
 
14
64
  return c.json({
15
65
  status: 'ok',
16
66
  timestamp: new Date().toISOString(),
17
-
18
- // Environment check
19
- env: {
20
- hasApiKey: !!c.env.PLAYCADEMY_API_KEY,
21
- hasGameId: !!c.env.GAME_ID,
22
- hasBaseUrl: !!c.env.PLAYCADEMY_BASE_URL,
23
- },
24
-
25
- // Config presence
26
- config: {
27
- hasConfig: !!config,
28
- hasIntegrations: !!config?.integrations,
29
- },
30
-
31
- // TimeBack status
32
- timeback: {
33
- enabled: !!config?.integrations?.timeback,
34
- courseIdFetched: !!sdk?.timeback?.courseId,
35
- },
36
-
37
- // Custom routes info
38
- customRoutes:
39
- c.get('customRoutes')?.map(r => ({
40
- path: r.path,
41
- methods: r.methods,
42
- })) || [],
67
+ env: { configured: isEnvConfigured(c.env) },
68
+ secrets: getSecretsCount(c.env),
69
+ integrations: getEnabledIntegrations(config),
70
+ routes: formatRoutes(routeMetadata),
71
+ ...getTimebackDebugInfo(config, sdk),
43
72
  })
44
73
  }
@@ -2,31 +2,59 @@
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.
8
5
  */
9
6
 
10
7
  import { ROUTES } from '../constants'
11
8
 
12
9
  import type { Context } from 'hono'
10
+ import type { PlaycademyConfig } from '@playcademy/sdk/server'
11
+ import type { RouteMetadata } from '../entry/types'
13
12
  import type { HonoEnv } from '../types'
14
13
 
15
- export async function GET(c: Context<HonoEnv>): Promise<Response> {
16
- const config = c.get('config')
17
- const customRoutes = c.get('customRoutes') || []
18
- const routes: string[] = [`GET ${ROUTES.INDEX}`, `GET ${ROUTES.HEALTH}`]
14
+ interface RouteInfo {
15
+ path: string
16
+ methods: string[]
17
+ }
19
18
 
20
- // Add TimeBack routes if configured
21
- if (config.integrations?.timeback) {
22
- routes.push(`POST ${ROUTES.TIMEBACK.END_ACTIVITY}`)
23
- }
19
+ /**
20
+ * Get built-in routes that are always available
21
+ */
22
+ function getBuiltinRoutes(): RouteInfo[] {
23
+ return [
24
+ { path: ROUTES.INDEX, methods: ['GET'] },
25
+ { path: ROUTES.HEALTH, methods: ['GET'] },
26
+ ]
27
+ }
24
28
 
25
- // Add custom routes
26
- for (const route of customRoutes) {
27
- const methods = route.methods?.join(', ') || '*'
28
- routes.push(`${methods} ${route.path}`)
29
+ /**
30
+ * Get TimeBack integration routes if enabled
31
+ */
32
+ function getTimebackRoutes(config?: PlaycademyConfig): RouteInfo[] {
33
+ if (config?.integrations?.timeback) {
34
+ return [{ path: ROUTES.TIMEBACK.END_ACTIVITY, methods: ['POST'] }]
29
35
  }
36
+ return []
37
+ }
38
+
39
+ /**
40
+ * Format custom routes from build-time metadata
41
+ */
42
+ function formatCustomRoutes(routes: RouteMetadata[]): RouteInfo[] {
43
+ return routes.map(r => ({
44
+ path: r.path,
45
+ methods: r.methods || ['*'],
46
+ }))
47
+ }
48
+
49
+ export async function GET(c: Context<HonoEnv>): Promise<Response> {
50
+ const config = c.get('config')
51
+ const routeMetadata = c.get('routeMetadata')
52
+
53
+ const routes = [
54
+ ...getBuiltinRoutes(),
55
+ ...getTimebackRoutes(config),
56
+ ...formatCustomRoutes(routeMetadata),
57
+ ]
30
58
 
31
59
  return c.json({
32
60
  name: config.name,
@@ -10,6 +10,7 @@
10
10
  /// <reference types="@cloudflare/workers-types" />
11
11
 
12
12
  import type { PlaycademyClient, PlaycademyConfig } from '@playcademy/sdk/server'
13
+ import type { RouteMetadata } from './entry/types'
13
14
 
14
15
  /**
15
16
  * Enabled integrations from playcademy.config.js
@@ -35,6 +36,9 @@ export interface ServerEnv {
35
36
  /** Platform base URL (stage-aware: hub.playcademy.net or hub.dev.playcademy.net) */
36
37
  PLAYCADEMY_BASE_URL: string
37
38
 
39
+ /** Game-specific secrets (optional) */
40
+ secrets?: Record<string, string>
41
+
38
42
  /** KV namespace binding (optional, Cloudflare-specific) */
39
43
  KV?: KVNamespace
40
44
 
@@ -43,6 +47,9 @@ export interface ServerEnv {
43
47
 
44
48
  /** R2 bucket binding (optional, Cloudflare-specific) */
45
49
  BUCKET?: R2Bucket
50
+
51
+ /** Allow dynamic secret bindings (secrets_KEY_NAME) */
52
+ [key: string]: unknown
46
53
  }
47
54
 
48
55
  /**
@@ -51,7 +58,7 @@ export interface ServerEnv {
51
58
  export interface HonoVariables {
52
59
  sdk: PlaycademyClient
53
60
  config: PlaycademyConfig
54
- customRoutes: Array<{ path: string; file: string; methods?: string[] }>
61
+ routeMetadata: Array<RouteMetadata>
55
62
  }
56
63
 
57
64
  /**