playcademy 0.13.16 → 0.13.17

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/index.js CHANGED
@@ -8840,16 +8840,18 @@ function setupCleanupHandlers(workspace, getServer) {
8840
8840
  async function setupServerHotReload(serverRef, port, workspace, config, loggerEnabled) {
8841
8841
  return startHotReload(
8842
8842
  async () => {
8843
- if (serverRef.current) {
8844
- await serverRef.current.dispose();
8845
- }
8846
8843
  const newConfig = await loadConfig();
8847
- const { server } = await startDevServer({
8844
+ await bundleBackend(newConfig, { sourcemap: false, minify: false });
8845
+ const oldServer = serverRef.current;
8846
+ if (oldServer) {
8847
+ await oldServer.dispose();
8848
+ }
8849
+ const { server: newServer } = await startDevServer({
8848
8850
  port,
8849
8851
  config: newConfig,
8850
8852
  logger: loggerEnabled
8851
8853
  });
8852
- serverRef.current = server;
8854
+ serverRef.current = newServer;
8853
8855
  await discoverRoutes(getCustomRoutesDirectory(workspace, newConfig));
8854
8856
  },
8855
8857
  { config }
@@ -4,42 +4,35 @@
4
4
  * This route will be available at: https://<your-game-slug>.playcademy.gg/api/sample/bucket
5
5
  */
6
6
 
7
- /**
8
- * File metadata for bucket storage
9
- */
7
+ import type { Context } from 'hono'
8
+
10
9
  interface FileInfo {
11
- /** File key in bucket */
12
10
  key: string
13
- /** File size in bytes */
11
+ name: string
14
12
  size: number
15
- /** Upload timestamp */
16
13
  uploaded: string
14
+ extension: string
17
15
  }
18
16
 
19
17
  /**
20
18
  * GET /api/sample/bucket
21
19
  *
22
- * List or retrieve files from bucket storage
20
+ * List all files or retrieve a specific file by key
21
+ * Query params:
22
+ * - key: File key to download (optional)
23
+ * - prefix: Filter files by prefix (optional)
23
24
  */
24
25
  export async function GET(c: Context): Promise<Response> {
25
26
  try {
26
27
  const fileKey = c.req.query('key')
27
28
 
28
- // If key provided, get specific file
29
29
  if (fileKey) {
30
30
  const object = await c.env.BUCKET.get(fileKey)
31
31
 
32
32
  if (!object) {
33
- return c.json(
34
- {
35
- success: false,
36
- error: 'File not found',
37
- },
38
- 404,
39
- )
33
+ return c.json({ success: false, error: 'File not found' }, 404)
40
34
  }
41
35
 
42
- // Return file with appropriate headers
43
36
  return new Response(object.body, {
44
37
  headers: {
45
38
  'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream',
@@ -49,28 +42,28 @@ export async function GET(c: Context): Promise<Response> {
49
42
  })
50
43
  }
51
44
 
52
- // Otherwise, list files
53
45
  const prefix = c.req.query('prefix') || ''
54
46
  const listed = await c.env.BUCKET.list({ prefix })
55
47
 
56
- const files: FileInfo[] = listed.objects.map(obj => ({
57
- key: obj.key,
58
- size: obj.size,
59
- uploaded: obj.uploaded.toISOString(),
60
- }))
48
+ const files: FileInfo[] = (listed.objects as Array<{ key: string; size: number; uploaded: Date }>).map(obj => {
49
+ const fileName = obj.key.split('/').pop() || obj.key
50
+ const extensionMatch = fileName.match(/\.([^.]+)$/)
51
+ return {
52
+ key: obj.key,
53
+ name: fileName,
54
+ size: obj.size,
55
+ uploaded: obj.uploaded.toISOString(),
56
+ extension: extensionMatch?.[1]?.toLowerCase() || 'unknown',
57
+ }
58
+ })
61
59
 
62
60
  return c.json({
63
61
  success: true,
64
- data: files,
65
- truncated: listed.truncated,
62
+ data: { files, total: files.length, truncated: listed.truncated },
66
63
  })
67
64
  } catch (error) {
68
65
  return c.json(
69
- {
70
- success: false,
71
- error: 'Failed to access bucket storage',
72
- details: error instanceof Error ? error.message : String(error),
73
- },
66
+ { success: false, error: 'Failed to list files', details: error instanceof Error ? error.message : 'Unknown error' },
74
67
  500,
75
68
  )
76
69
  }
@@ -80,60 +73,60 @@ export async function GET(c: Context): Promise<Response> {
80
73
  * PUT /api/sample/bucket
81
74
  *
82
75
  * Upload a file to bucket storage
76
+ * Query params:
77
+ * - name: File name (required)
83
78
  */
84
79
  export async function PUT(c: Context): Promise<Response> {
85
80
  try {
86
- const fileKey = c.req.query('key')
87
-
88
- if (!fileKey) {
89
- return c.json(
90
- {
91
- success: false,
92
- error: 'File key is required',
93
- },
94
- 400,
95
- )
81
+ const fileName = c.req.query('name')
82
+ if (!fileName) {
83
+ return c.json({ success: false, error: 'name query param is required' }, 400)
96
84
  }
97
85
 
98
- // Get file from request body
99
86
  const body = await c.req.arrayBuffer()
100
-
101
87
  if (!body || body.byteLength === 0) {
102
- return c.json(
103
- {
104
- success: false,
105
- error: 'File body is required',
106
- },
107
- 400,
108
- )
88
+ return c.json({ success: false, error: 'File body is required' }, 400)
109
89
  }
110
90
 
111
- // Get content type from header or default to binary
112
- const contentType = c.req.header('Content-Type') || 'application/octet-stream'
91
+ // Validate file size (10MB limit)
92
+ const maxSize = 10 * 1024 * 1024
93
+ if (body.byteLength > maxSize) {
94
+ return c.json({ success: false, error: `File exceeds ${maxSize / 1024 / 1024}MB limit` }, 400)
95
+ }
113
96
 
114
- // Upload to bucket
115
- await c.env.BUCKET.put(fileKey, body, {
116
- httpMetadata: {
117
- contentType,
118
- },
119
- })
97
+ // Sanitize name and create key with timestamp
98
+ const safeName = fileName.replace(/[^a-zA-Z0-9._-]/g, '_')
99
+ const fileKey = `uploads/${Date.now()}-${safeName}`
100
+
101
+ // Map extension to content-type
102
+ const extension = safeName.split('.').pop()?.toLowerCase() || ''
103
+ const contentTypeMap: Record<string, string> = {
104
+ jpg: 'image/jpeg',
105
+ jpeg: 'image/jpeg',
106
+ png: 'image/png',
107
+ gif: 'image/gif',
108
+ webp: 'image/webp',
109
+ pdf: 'application/pdf',
110
+ txt: 'text/plain',
111
+ json: 'application/json',
112
+ }
113
+ const contentType = contentTypeMap[extension] || c.req.header('Content-Type') || 'application/octet-stream'
114
+
115
+ await c.env.BUCKET.put(fileKey, body, { httpMetadata: { contentType } })
120
116
 
121
117
  return c.json({
122
118
  success: true,
123
119
  data: {
124
120
  key: fileKey,
121
+ name: safeName,
125
122
  size: body.byteLength,
126
123
  uploaded: new Date().toISOString(),
124
+ extension: extension || 'unknown',
127
125
  },
128
- message: 'File uploaded successfully',
129
126
  })
130
127
  } catch (error) {
131
128
  return c.json(
132
- {
133
- success: false,
134
- error: 'Failed to upload file',
135
- details: error instanceof Error ? error.message : String(error),
136
- },
129
+ { success: false, error: 'Failed to upload file', details: error instanceof Error ? error.message : 'Unknown error' },
137
130
  500,
138
131
  )
139
132
  }
@@ -143,35 +136,22 @@ export async function PUT(c: Context): Promise<Response> {
143
136
  * DELETE /api/sample/bucket
144
137
  *
145
138
  * Delete a file from bucket storage
139
+ * Query params:
140
+ * - key: File key to delete (required)
146
141
  */
147
142
  export async function DELETE(c: Context): Promise<Response> {
148
143
  try {
149
144
  const fileKey = c.req.query('key')
150
145
 
151
146
  if (!fileKey) {
152
- return c.json(
153
- {
154
- success: false,
155
- error: 'File key is required',
156
- },
157
- 400,
158
- )
147
+ return c.json({ success: false, error: 'key query param is required' }, 400)
159
148
  }
160
149
 
161
- // Delete from bucket
162
150
  await c.env.BUCKET.delete(fileKey)
163
-
164
- return c.json({
165
- success: true,
166
- message: 'File deleted successfully',
167
- })
151
+ return c.json({ success: true, message: 'File deleted successfully' })
168
152
  } catch (error) {
169
153
  return c.json(
170
- {
171
- success: false,
172
- error: 'Failed to delete file',
173
- details: error instanceof Error ? error.message : String(error),
174
- },
154
+ { success: false, error: 'Failed to delete file', details: error instanceof Error ? error.message : 'Unknown error' },
175
155
  500,
176
156
  )
177
157
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playcademy",
3
- "version": "0.13.16",
3
+ "version": "0.13.17",
4
4
  "type": "module",
5
5
  "module": "./dist/index.js",
6
6
  "main": "./dist/index.js",
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@inquirer/prompts": "^7.8.6",
43
- "@playcademy/sdk": "0.1.6",
43
+ "@playcademy/sdk": "0.1.7",
44
44
  "better-sqlite3": "^12.4.1",
45
45
  "chokidar": "^4.0.3",
46
46
  "colorette": "^2.0.20",