playcademy 0.14.12 → 0.14.13

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/db.d.ts CHANGED
@@ -75,6 +75,17 @@ declare function resetDatabase(workspace: string, mf: Miniflare, options?: {
75
75
  debug?: boolean;
76
76
  }): Promise<void>;
77
77
 
78
+ /**
79
+ * Environment context passed to seed functions
80
+ */
81
+ interface SeedContext {
82
+ env: {
83
+ DB: unknown;
84
+ BUCKET?: unknown;
85
+ secrets?: Record<string, string>;
86
+ };
87
+ }
88
+
78
89
  /**
79
90
  * Database Seed Utilities
80
91
  *
@@ -87,19 +98,17 @@ declare function resetDatabase(workspace: string, mf: Miniflare, options?: {
87
98
  * Bundles all deps for portability.
88
99
  */
89
100
  declare function importSeedModule(seedPath: string): Promise<{
90
- seed?: (c: {
91
- env: {
92
- DB: unknown;
93
- };
94
- }) => Promise<void>;
101
+ seed?: (c: SeedContext) => Promise<void>;
95
102
  }>;
103
+ declare function getBucket(mf: Miniflare): Promise<unknown | null>;
96
104
  /**
97
- * Execute a seed file against a Miniflare D1 instance
105
+ * Execute a seed file against a Miniflare instance
98
106
  *
99
107
  * @param seedFilePath - Path to seed file
100
- * @param mf - Miniflare instance with D1 database
108
+ * @param mf - Miniflare instance with D1 database (and optionally bucket/secrets)
109
+ * @param envSecrets - Environment secrets from .env file
101
110
  */
102
- declare function executeSeedFile(seedFilePath: string, mf: Miniflare): Promise<void>;
111
+ declare function executeSeedFile(seedFilePath: string, mf: Miniflare, envSecrets?: Record<string, string>): Promise<void>;
103
112
 
104
- export { bundleSeedWorker, executeSeedFile, getDevDbPath as getPath, importSeedModule, resetDatabase };
113
+ export { bundleSeedWorker, executeSeedFile, getBucket, getDevDbPath as getPath, importSeedModule, resetDatabase };
105
114
  export type { SeedWorkerBundle };
package/dist/db.js CHANGED
@@ -2938,7 +2938,14 @@ async function resetDatabase(workspace, mf, options = { debug: false }) {
2938
2938
  async function importSeedModule(seedPath) {
2939
2939
  return await importTypescriptFile(seedPath);
2940
2940
  }
2941
- async function executeSeedFile(seedFilePath, mf) {
2941
+ async function getBucket(mf) {
2942
+ try {
2943
+ return await mf.getR2Bucket(CLOUDFLARE_BINDINGS.BUCKET);
2944
+ } catch {
2945
+ return null;
2946
+ }
2947
+ }
2948
+ async function executeSeedFile(seedFilePath, mf, envSecrets = {}) {
2942
2949
  const d1 = await mf.getD1Database(CLOUDFLARE_BINDINGS.DB);
2943
2950
  const seedModule = await importSeedModule(seedFilePath);
2944
2951
  if (typeof seedModule.seed !== "function") {
@@ -2951,9 +2958,16 @@ async function executeSeedFile(seedFilePath, mf) {
2951
2958
  logger.newLine();
2952
2959
  process.exit(1);
2953
2960
  }
2961
+ const bucket = await getBucket(mf);
2962
+ const hasSecrets = Object.keys(envSecrets).length > 0;
2954
2963
  await runStep(
2955
2964
  "Seeding database...",
2956
- async () => seedModule.seed?.({ env: { DB: d1 } }),
2965
+ async () => {
2966
+ const env = { DB: d1 };
2967
+ if (bucket) env.BUCKET = bucket;
2968
+ if (hasSecrets) env.secrets = envSecrets;
2969
+ return seedModule.seed?.({ env });
2970
+ },
2957
2971
  "Database seeded successfully!"
2958
2972
  );
2959
2973
  logger.newLine();
@@ -2961,6 +2975,7 @@ async function executeSeedFile(seedFilePath, mf) {
2961
2975
  export {
2962
2976
  bundleSeedWorker,
2963
2977
  executeSeedFile,
2978
+ getBucket,
2964
2979
  getDevDbPath as getPath,
2965
2980
  importSeedModule,
2966
2981
  resetDatabase
@@ -177,8 +177,9 @@ async function serveFromR2(c: Context<HonoEnv>, bucket: R2Bucket): Promise<Respo
177
177
  return new Response(object.body, {
178
178
  headers: {
179
179
  'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream',
180
- 'Cache-Control':
181
- key === 'index.html' ? 'no-cache' : 'public, max-age=31536000, immutable',
180
+ // Use 1-hour caching - balances performance with update propagation
181
+ // (Godot and other engines use non-hashed filenames)
182
+ 'Cache-Control': key === 'index.html' ? 'no-cache' : 'public, max-age=3600',
182
183
  ETag: object.etag,
183
184
  },
184
185
  })
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Stub Worker for Static-Only Games
3
+ *
4
+ * Minimal worker that serves static assets with CORS headers.
5
+ * Used for games that don't have custom backend routes.
6
+ *
7
+ * Features:
8
+ * - Serves files from Workers Assets or R2 bucket binding
9
+ * - Supports hybrid deployment strategy (small files via Assets, large files via R2)
10
+ * - Adds CORS headers for cross-origin manifest fetching
11
+ * - Handles OPTIONS preflight requests
12
+ * - Returns 404 for /api/* routes (no backend deployed)
13
+ * - Supports SPA routing (falls back to index.html)
14
+ */
15
+
16
+ type AssetsFetcher = { fetch(request: Request): Promise<Response> }
17
+ type R2Bucket = { get(key: string): Promise<R2Object | null> }
18
+ type R2Object = { body: ReadableStream; httpMetadata?: { contentType?: string }; etag: string }
19
+
20
+ interface Env {
21
+ ASSETS: AssetsFetcher | R2Bucket
22
+ }
23
+
24
+ /**
25
+ * Detect if ASSETS binding is an R2 bucket or Workers Assets Fetcher
26
+ */
27
+ function isR2Bucket(assets: AssetsFetcher | R2Bucket): assets is R2Bucket {
28
+ return 'get' in assets && typeof assets.get === 'function' && !('fetch' in assets)
29
+ }
30
+
31
+ /**
32
+ * Serve from Workers Assets (default for files <25MB)
33
+ */
34
+ async function serveFromAssets(request: Request, assets: AssetsFetcher): Promise<Response> {
35
+ const response = await assets.fetch(request)
36
+
37
+ if (response.status !== 404) {
38
+ // Add CORS headers
39
+ const modifiedResponse = new Response(response.body, response)
40
+ modifiedResponse.headers.set('Access-Control-Allow-Origin', '*')
41
+ modifiedResponse.headers.set('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS')
42
+ return modifiedResponse
43
+ }
44
+
45
+ // SPA routing: if not a file request, try index.html
46
+ const path = new URL(request.url).pathname
47
+ if (path.includes('.')) {
48
+ return response // File request not found
49
+ }
50
+
51
+ const indexUrl = new URL(request.url)
52
+ indexUrl.pathname = '/index.html'
53
+ const indexResponse = await assets.fetch(new Request(indexUrl.toString()))
54
+
55
+ // Add CORS headers to index.html response
56
+ const modifiedResponse = new Response(indexResponse.body, indexResponse)
57
+ modifiedResponse.headers.set('Access-Control-Allow-Origin', '*')
58
+ modifiedResponse.headers.set('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS')
59
+ return modifiedResponse
60
+ }
61
+
62
+ /**
63
+ * Serve from R2 bucket (for large files like Godot WASM)
64
+ */
65
+ async function serveFromR2(request: Request, bucket: R2Bucket): Promise<Response> {
66
+ const path = new URL(request.url).pathname
67
+ const key = path === '/' ? 'index.html' : path.slice(1)
68
+
69
+ const object = await bucket.get(key)
70
+
71
+ if (object) {
72
+ return new Response(object.body, {
73
+ headers: {
74
+ 'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream',
75
+ // Use 1-hour caching - balances performance with update propagation
76
+ // (Godot and other engines use non-hashed filenames)
77
+ 'Cache-Control': key === 'index.html' ? 'no-cache' : 'public, max-age=3600',
78
+ ETag: object.etag,
79
+ 'Access-Control-Allow-Origin': '*',
80
+ 'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
81
+ },
82
+ })
83
+ }
84
+
85
+ // SPA routing: if not a file request, try index.html
86
+ if (path.includes('.')) {
87
+ return new Response('Not Found', { status: 404 })
88
+ }
89
+
90
+ const indexObject = await bucket.get('index.html')
91
+ if (indexObject) {
92
+ return new Response(indexObject.body, {
93
+ headers: {
94
+ 'Content-Type': 'text/html',
95
+ 'Cache-Control': 'no-cache',
96
+ 'Access-Control-Allow-Origin': '*',
97
+ 'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
98
+ },
99
+ })
100
+ }
101
+
102
+ return new Response('Not Found', { status: 404 })
103
+ }
104
+
105
+ export default {
106
+ async fetch(request: Request, env: Env): Promise<Response> {
107
+ try {
108
+ const url = new URL(request.url)
109
+
110
+ // Handle CORS preflight
111
+ if (request.method === 'OPTIONS') {
112
+ const headers = new Headers()
113
+ headers.set('Access-Control-Allow-Origin', '*')
114
+ headers.set('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS')
115
+ const reqHeaders = request.headers.get('Access-Control-Request-Headers')
116
+ if (reqHeaders) {
117
+ headers.set('Access-Control-Allow-Headers', reqHeaders)
118
+ }
119
+ headers.set('Access-Control-Max-Age', '86400')
120
+ return new Response(null, { status: 204, headers })
121
+ }
122
+
123
+ // No backend routes available
124
+ if (url.pathname.startsWith('/api/')) {
125
+ return new Response('No backend deployed', { status: 404 })
126
+ }
127
+
128
+ // Serve static assets (auto-detect Workers Assets vs R2)
129
+ if (!env.ASSETS) {
130
+ return new Response('ASSETS binding not configured', { status: 500 })
131
+ }
132
+
133
+ return isR2Bucket(env.ASSETS)
134
+ ? await serveFromR2(request, env.ASSETS)
135
+ : await serveFromAssets(request, env.ASSETS)
136
+ } catch (error) {
137
+ return new Response('Error: ' + (error as Error).message, { status: 500 })
138
+ }
139
+ },
140
+ }
package/dist/index.js CHANGED
@@ -5680,7 +5680,7 @@ function getDevDbPath() {
5680
5680
 
5681
5681
  // src/lib/db/bundle-seed.ts
5682
5682
  async function bundleSeedWorker(seedFilePath, projectPath) {
5683
- const esbuild2 = await import("esbuild");
5683
+ const esbuild3 = await import("esbuild");
5684
5684
  const entryCode = `
5685
5685
  import { seed } from '${seedFilePath}'
5686
5686
 
@@ -5718,7 +5718,7 @@ async function bundleSeedWorker(seedFilePath, projectPath) {
5718
5718
  sourcemap: false,
5719
5719
  logLevel: "error"
5720
5720
  };
5721
- const result = await esbuild2.build(buildConfig);
5721
+ const result = await esbuild3.build(buildConfig);
5722
5722
  if (!result.outputFiles?.[0]) {
5723
5723
  throw new Error("Seed worker bundling failed: no output");
5724
5724
  }
@@ -5770,7 +5770,14 @@ init_core();
5770
5770
  async function importSeedModule(seedPath) {
5771
5771
  return await importTypescriptFile(seedPath);
5772
5772
  }
5773
- async function executeSeedFile(seedFilePath, mf) {
5773
+ async function getBucket(mf) {
5774
+ try {
5775
+ return await mf.getR2Bucket(CLOUDFLARE_BINDINGS.BUCKET);
5776
+ } catch {
5777
+ return null;
5778
+ }
5779
+ }
5780
+ async function executeSeedFile(seedFilePath, mf, envSecrets = {}) {
5774
5781
  const d1 = await mf.getD1Database(CLOUDFLARE_BINDINGS.DB);
5775
5782
  const seedModule = await importSeedModule(seedFilePath);
5776
5783
  if (typeof seedModule.seed !== "function") {
@@ -5783,9 +5790,16 @@ async function executeSeedFile(seedFilePath, mf) {
5783
5790
  logger.newLine();
5784
5791
  process.exit(1);
5785
5792
  }
5793
+ const bucket = await getBucket(mf);
5794
+ const hasSecrets = Object.keys(envSecrets).length > 0;
5786
5795
  await runStep(
5787
5796
  "Seeding database...",
5788
- async () => seedModule.seed?.({ env: { DB: d1 } }),
5797
+ async () => {
5798
+ const env = { DB: d1 };
5799
+ if (bucket) env.BUCKET = bucket;
5800
+ if (hasSecrets) env.secrets = envSecrets;
5801
+ return seedModule.seed?.({ env });
5802
+ },
5789
5803
  "Database seeded successfully!"
5790
5804
  );
5791
5805
  logger.newLine();
@@ -6031,8 +6045,16 @@ init_constants2();
6031
6045
  function textLoaderPlugin() {
6032
6046
  return {
6033
6047
  name: "text-loader",
6034
- setup(build2) {
6035
- build2.onLoad({ filter: /edge-play\/src\/entry\.ts$/ }, async (args) => {
6048
+ setup(build3) {
6049
+ build3.onLoad({ filter: /edge-play\/src\/entry\.ts$/ }, async (args) => {
6050
+ const fs2 = await import("fs/promises");
6051
+ const text5 = await fs2.readFile(args.path, "utf8");
6052
+ return {
6053
+ contents: `export default ${JSON.stringify(text5)}`,
6054
+ loader: "js"
6055
+ };
6056
+ });
6057
+ build3.onLoad({ filter: /edge-play\/src\/stub-entry\.ts$/ }, async (args) => {
6036
6058
  const fs2 = await import("fs/promises");
6037
6059
  const text5 = await fs2.readFile(args.path, "utf8");
6038
6060
  return {
@@ -6040,7 +6062,7 @@ function textLoaderPlugin() {
6040
6062
  loader: "js"
6041
6063
  };
6042
6064
  });
6043
- build2.onLoad({ filter: /edge-play\/src\/routes\/root\.html$/ }, async (args) => {
6065
+ build3.onLoad({ filter: /edge-play\/src\/routes\/root\.html$/ }, async (args) => {
6044
6066
  const fs2 = await import("fs/promises");
6045
6067
  const text5 = await fs2.readFile(args.path, "utf8");
6046
6068
  return {
@@ -6048,7 +6070,7 @@ function textLoaderPlugin() {
6048
6070
  loader: "js"
6049
6071
  };
6050
6072
  });
6051
- build2.onLoad({ filter: /templates\/sample-route\.ts$/ }, async (args) => {
6073
+ build3.onLoad({ filter: /templates\/sample-route\.ts$/ }, async (args) => {
6052
6074
  const fs2 = await import("fs/promises");
6053
6075
  const text5 = await fs2.readFile(args.path, "utf8");
6054
6076
  return {
@@ -6162,8 +6184,8 @@ async function transpileRoute(filePath) {
6162
6184
  if (isBun() || !filePath.endsWith(".ts")) {
6163
6185
  return filePath;
6164
6186
  }
6165
- const esbuild2 = await import("esbuild");
6166
- const result = await esbuild2.build({
6187
+ const esbuild3 = await import("esbuild");
6188
+ const result = await esbuild3.build({
6167
6189
  entryPoints: [filePath],
6168
6190
  write: false,
6169
6191
  format: "esm",
@@ -6319,7 +6341,7 @@ function createEsbuildConfig(entryCode, paths, bundleConfig, customRoutesDir, op
6319
6341
  };
6320
6342
  }
6321
6343
  async function bundleBackend(config, options = {}) {
6322
- const esbuild2 = await import("esbuild");
6344
+ const esbuild3 = await import("esbuild");
6323
6345
  const { customRouteData, customRoutesDir } = await discoverCustomRoutes(config);
6324
6346
  const bundleConfig = {
6325
6347
  ...config,
@@ -6335,7 +6357,7 @@ async function bundleBackend(config, options = {}) {
6335
6357
  customRoutesDir,
6336
6358
  options
6337
6359
  );
6338
- const result = await esbuild2.build(buildConfig);
6360
+ const result = await esbuild3.build(buildConfig);
6339
6361
  if (!result.outputFiles?.[0]) {
6340
6362
  throw new Error("Backend bundling failed: no output");
6341
6363
  }
@@ -6618,14 +6640,14 @@ function formatDelta(bytes) {
6618
6640
  return `${arrow} ${value.toFixed(2)} ${unit}`;
6619
6641
  }
6620
6642
  function displayDeploymentDiff(options) {
6621
- const { diff, noChanges, build: build2, backend, integrations } = options;
6643
+ const { diff, noChanges, build: build3, backend, integrations } = options;
6622
6644
  if (noChanges) {
6623
6645
  logger.remark("No changes detected");
6624
6646
  logger.newLine();
6625
6647
  return;
6626
6648
  }
6627
6649
  const hasConfigChanges = Object.keys(diff).length > 0;
6628
- const buildChanged = build2?.changed === true;
6650
+ const buildChanged = build3?.changed === true;
6629
6651
  const backendChanged = backend?.changed === true;
6630
6652
  const forceBackend = backend?.forced;
6631
6653
  const schemaStatementCount = backend?.schemaStatementCount;
@@ -6661,8 +6683,8 @@ function displayDeploymentDiff(options) {
6661
6683
  }
6662
6684
  if (buildChanged) {
6663
6685
  logger.bold("Frontend", 1);
6664
- const previousSize = build2?.previousSize;
6665
- const currentSize = build2?.currentSize;
6686
+ const previousSize = build3?.previousSize;
6687
+ const currentSize = build3?.currentSize;
6666
6688
  if (previousSize !== void 0 && currentSize !== void 0) {
6667
6689
  logger.sizeChange("Build", previousSize, currentSize, formatSize, formatDelta, 2);
6668
6690
  } else if (currentSize !== void 0) {
@@ -7123,8 +7145,11 @@ function scaffoldProtectedExample(workspace) {
7123
7145
  writeFileSync3(join14(sampleDir, "protected.ts"), protectedRouteTemplate);
7124
7146
  }
7125
7147
  function updateEnvForAuth(workspace, strategies) {
7126
- if (strategies.length === 0) return;
7127
7148
  const envLines = [];
7149
+ envLines.push("# Better Auth (required)");
7150
+ envLines.push("# Generate with: openssl rand -base64 32");
7151
+ envLines.push("BETTER_AUTH_SECRET=your_secret_here");
7152
+ envLines.push("");
7128
7153
  if (strategies.includes("github")) {
7129
7154
  envLines.push("# GitHub OAuth (for standalone auth)");
7130
7155
  envLines.push("GITHUB_CLIENT_ID=your_github_client_id");
@@ -7137,9 +7162,7 @@ function updateEnvForAuth(workspace, strategies) {
7137
7162
  envLines.push("GOOGLE_CLIENT_SECRET=your_google_client_secret");
7138
7163
  envLines.push("");
7139
7164
  }
7140
- if (envLines.length > 0) {
7141
- updateEnvExample(workspace, envLines);
7142
- }
7165
+ updateEnvExample(workspace, envLines);
7143
7166
  }
7144
7167
  async function scaffoldAuthSetup(options = {}) {
7145
7168
  const workspace = getWorkspace();
@@ -7195,7 +7218,7 @@ import { join as join15 } from "path";
7195
7218
  // package.json
7196
7219
  var package_default2 = {
7197
7220
  name: "playcademy",
7198
- version: "0.14.11",
7221
+ version: "0.14.12",
7199
7222
  type: "module",
7200
7223
  exports: {
7201
7224
  ".": {
@@ -9398,6 +9421,35 @@ import { existsSync as existsSync20 } from "fs";
9398
9421
  import { readFile as readFile4 } from "fs/promises";
9399
9422
  import { basename as basename2, join as join24, resolve as resolve8 } from "path";
9400
9423
 
9424
+ // src/lib/deploy/stub.ts
9425
+ import * as esbuild2 from "esbuild";
9426
+
9427
+ // ../edge-play/src/stub-entry.ts
9428
+ var stub_entry_default = "/**\n * Stub Worker for Static-Only Games\n *\n * Minimal worker that serves static assets with CORS headers.\n * Used for games that don't have custom backend routes.\n *\n * Features:\n * - Serves files from Workers Assets or R2 bucket binding\n * - Supports hybrid deployment strategy (small files via Assets, large files via R2)\n * - Adds CORS headers for cross-origin manifest fetching\n * - Handles OPTIONS preflight requests\n * - Returns 404 for /api/* routes (no backend deployed)\n * - Supports SPA routing (falls back to index.html)\n */\n\ntype AssetsFetcher = { fetch(request: Request): Promise<Response> }\ntype R2Bucket = { get(key: string): Promise<R2Object | null> }\ntype R2Object = { body: ReadableStream; httpMetadata?: { contentType?: string }; etag: string }\n\ninterface Env {\n ASSETS: AssetsFetcher | R2Bucket\n}\n\n/**\n * Detect if ASSETS binding is an R2 bucket or Workers Assets Fetcher\n */\nfunction isR2Bucket(assets: AssetsFetcher | R2Bucket): assets is R2Bucket {\n return 'get' in assets && typeof assets.get === 'function' && !('fetch' in assets)\n}\n\n/**\n * Serve from Workers Assets (default for files <25MB)\n */\nasync function serveFromAssets(request: Request, assets: AssetsFetcher): Promise<Response> {\n const response = await assets.fetch(request)\n\n if (response.status !== 404) {\n // Add CORS headers\n const modifiedResponse = new Response(response.body, response)\n modifiedResponse.headers.set('Access-Control-Allow-Origin', '*')\n modifiedResponse.headers.set('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS')\n return modifiedResponse\n }\n\n // SPA routing: if not a file request, try index.html\n const path = new URL(request.url).pathname\n if (path.includes('.')) {\n return response // File request not found\n }\n\n const indexUrl = new URL(request.url)\n indexUrl.pathname = '/index.html'\n const indexResponse = await assets.fetch(new Request(indexUrl.toString()))\n\n // Add CORS headers to index.html response\n const modifiedResponse = new Response(indexResponse.body, indexResponse)\n modifiedResponse.headers.set('Access-Control-Allow-Origin', '*')\n modifiedResponse.headers.set('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS')\n return modifiedResponse\n}\n\n/**\n * Serve from R2 bucket (for large files like Godot WASM)\n */\nasync function serveFromR2(request: Request, bucket: R2Bucket): Promise<Response> {\n const path = new URL(request.url).pathname\n const key = path === '/' ? 'index.html' : path.slice(1)\n\n const object = await bucket.get(key)\n\n if (object) {\n return new Response(object.body, {\n headers: {\n 'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream',\n // Use 1-hour caching - balances performance with update propagation\n // (Godot and other engines use non-hashed filenames)\n 'Cache-Control': key === 'index.html' ? 'no-cache' : 'public, max-age=3600',\n ETag: object.etag,\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',\n },\n })\n }\n\n // SPA routing: if not a file request, try index.html\n if (path.includes('.')) {\n return new Response('Not Found', { status: 404 })\n }\n\n const indexObject = await bucket.get('index.html')\n if (indexObject) {\n return new Response(indexObject.body, {\n headers: {\n 'Content-Type': 'text/html',\n 'Cache-Control': 'no-cache',\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',\n },\n })\n }\n\n return new Response('Not Found', { status: 404 })\n}\n\nexport default {\n async fetch(request: Request, env: Env): Promise<Response> {\n try {\n const url = new URL(request.url)\n\n // Handle CORS preflight\n if (request.method === 'OPTIONS') {\n const headers = new Headers()\n headers.set('Access-Control-Allow-Origin', '*')\n headers.set('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS')\n const reqHeaders = request.headers.get('Access-Control-Request-Headers')\n if (reqHeaders) {\n headers.set('Access-Control-Allow-Headers', reqHeaders)\n }\n headers.set('Access-Control-Max-Age', '86400')\n return new Response(null, { status: 204, headers })\n }\n\n // No backend routes available\n if (url.pathname.startsWith('/api/')) {\n return new Response('No backend deployed', { status: 404 })\n }\n\n // Serve static assets (auto-detect Workers Assets vs R2)\n if (!env.ASSETS) {\n return new Response('ASSETS binding not configured', { status: 500 })\n }\n\n return isR2Bucket(env.ASSETS)\n ? await serveFromR2(request, env.ASSETS)\n : await serveFromAssets(request, env.ASSETS)\n } catch (error) {\n return new Response('Error: ' + (error as Error).message, { status: 500 })\n }\n },\n}\n";
9429
+
9430
+ // src/lib/deploy/stub.ts
9431
+ var stubEntryTemplate = stub_entry_default.toString();
9432
+ async function bundleStubWorker() {
9433
+ const result = await esbuild2.build({
9434
+ stdin: {
9435
+ contents: stubEntryTemplate,
9436
+ loader: "ts"
9437
+ },
9438
+ bundle: true,
9439
+ format: "esm",
9440
+ platform: "browser",
9441
+ target: "es2022",
9442
+ write: false,
9443
+ minify: false,
9444
+ sourcemap: false,
9445
+ logLevel: "error"
9446
+ });
9447
+ if (!result.outputFiles?.[0]) {
9448
+ throw new Error("Failed to bundle stub worker");
9449
+ }
9450
+ return result.outputFiles[0].text;
9451
+ }
9452
+
9401
9453
  // src/lib/deploy/utils.ts
9402
9454
  init_src2();
9403
9455
  function getDeploymentId(gameSlug) {
@@ -9601,6 +9653,17 @@ async function deployGame(context2, shouldUploadBuild, shouldDeployBackend) {
9601
9653
  deployedSecrets: deploymentPrep.secretKeys,
9602
9654
  deployedAt: (/* @__PURE__ */ new Date()).toISOString()
9603
9655
  };
9656
+ } else if (buildFile && !shouldDeployBackend) {
9657
+ const stubCode = await runStep(
9658
+ "Bundling stub worker",
9659
+ async () => bundleStubWorker(),
9660
+ "Stub worker bundled",
9661
+ { silent: true }
9662
+ );
9663
+ backendBundle = {
9664
+ code: stubCode,
9665
+ config: fullConfig || { name: "stub" }
9666
+ };
9604
9667
  }
9605
9668
  let game;
9606
9669
  if (buildFile) {
@@ -11721,6 +11784,7 @@ async function runDbResetRemote(options) {
11721
11784
  async function runDbResetLocal(options) {
11722
11785
  const workspace = getWorkspace();
11723
11786
  const dbDir = join30(workspace, CLI_DIRECTORIES.DATABASE);
11787
+ logger.newLine();
11724
11788
  if (!existsSync23(dbDir)) {
11725
11789
  logger.warn("No database found to reset");
11726
11790
  logger.newLine();
@@ -11905,10 +11969,37 @@ async function runDbSeedRemote(seedFile, options) {
11905
11969
  async function runDbSeedLocal(seedFile, options) {
11906
11970
  const workspace = getWorkspace();
11907
11971
  const dbDir = join31(workspace, CLI_DIRECTORIES.DATABASE);
11972
+ const config = await loadConfig();
11973
+ const hasBucket = hasBucketSetup(config);
11974
+ const hasKV = hasKVSetup(config);
11975
+ const bucketDir = hasBucket ? join31(workspace, CLI_DIRECTORIES.BUCKET) : void 0;
11976
+ const kvDir = hasKV ? join31(workspace, CLI_DIRECTORIES.KV) : void 0;
11977
+ if (bucketDir) {
11978
+ const { mkdir: mkdir5 } = await import("fs/promises");
11979
+ await mkdir5(bucketDir, { recursive: true });
11980
+ }
11981
+ if (kvDir) {
11982
+ const { mkdir: mkdir5 } = await import("fs/promises");
11983
+ await mkdir5(kvDir, { recursive: true });
11984
+ }
11985
+ const envSecrets = await readEnvFile(workspace);
11986
+ const bindings = {};
11987
+ for (const [key, value] of Object.entries(envSecrets)) {
11988
+ bindings[`secrets_${key}`] = value;
11989
+ }
11908
11990
  const mf = new Miniflare3({
11909
11991
  modules: [{ type: "ESModule", path: "index.mjs", contents: "" }],
11992
+ bindings,
11910
11993
  d1Databases: [CLOUDFLARE_BINDINGS.DB],
11911
11994
  d1Persist: dbDir,
11995
+ ...hasBucket && {
11996
+ r2Buckets: [CLOUDFLARE_BINDINGS.BUCKET],
11997
+ r2Persist: bucketDir
11998
+ },
11999
+ ...hasKV && {
12000
+ kvNamespaces: [CLOUDFLARE_BINDINGS.KV],
12001
+ kvPersist: kvDir
12002
+ },
11912
12003
  compatibilityDate: CLOUDFLARE_COMPATIBILITY_DATE
11913
12004
  });
11914
12005
  logger.newLine();
@@ -11916,7 +12007,7 @@ async function runDbSeedLocal(seedFile, options) {
11916
12007
  await resetDatabase(workspace, mf, { debug: options.debug });
11917
12008
  }
11918
12009
  try {
11919
- await executeSeedFile(seedFile, mf);
12010
+ await executeSeedFile(seedFile, mf, envSecrets);
11920
12011
  } finally {
11921
12012
  await mf.dispose();
11922
12013
  }
@@ -13569,7 +13660,6 @@ async function runBucketListLocal(options) {
13569
13660
  logger.newLine();
13570
13661
  return;
13571
13662
  }
13572
- logger.success(`Found ${files.length} file${files.length === 1 ? "" : "s"}`);
13573
13663
  if (options.prefix) {
13574
13664
  logger.data("Prefix", options.prefix, 1);
13575
13665
  }
@@ -14954,6 +15044,7 @@ export {
14954
15044
  getAuthenticatedEnvironments,
14955
15045
  getBaseUrl,
14956
15046
  getBestUnit,
15047
+ getBucket,
14957
15048
  getBucketKey,
14958
15049
  getCallbackUrl,
14959
15050
  getCliContext,
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { desc } from 'drizzle-orm'
9
9
 
10
- import { getDb, schema } from '../../../db'
10
+ import { getDb, schema } from 'db'
11
11
 
12
12
  /**
13
13
  * GET /api/sample/database
@@ -11,10 +11,20 @@ import { playcademy } from '@playcademy/better-auth/server'
11
11
 
12
12
  import { getDb } from '../../db'
13
13
 
14
- import type { Context } from 'hono'
14
+ function getAuthSecret(c: Context): string {
15
+ const secret = c.env.secrets?.BETTER_AUTH_SECRET
16
+ if (!secret) {
17
+ throw new Error(
18
+ 'BETTER_AUTH_SECRET is required. ' +
19
+ 'Set it locally in .env or deploy with: playcademy secret set BETTER_AUTH_SECRET <value>'
20
+ )
21
+ }
22
+ return secret
23
+ }
15
24
 
16
25
  export function getAuth(c: Context) {
17
26
  const db = getDb(c.env.DB)
27
+ const secret = getAuthSecret(c)
18
28
 
19
29
  // CUSTOMIZABLE: Configure trusted origins for CORS
20
30
  // These origins are allowed to make cross-origin requests to your game's auth endpoints.
@@ -31,6 +41,8 @@ export function getAuth(c: Context) {
31
41
  usePlural: false,
32
42
  }),
33
43
 
44
+ secret,
45
+
34
46
  trustedOrigins,
35
47
  {{EMAIL_AND_PASSWORD}}{{SOCIAL_PROVIDERS}}
36
48
  // REQUIRED: Platform integration
package/dist/utils.js CHANGED
@@ -3521,6 +3521,14 @@ function textLoaderPlugin() {
3521
3521
  loader: "js"
3522
3522
  };
3523
3523
  });
3524
+ build2.onLoad({ filter: /edge-play\/src\/stub-entry\.ts$/ }, async (args) => {
3525
+ const fs2 = await import("fs/promises");
3526
+ const text = await fs2.readFile(args.path, "utf8");
3527
+ return {
3528
+ contents: `export default ${JSON.stringify(text)}`,
3529
+ loader: "js"
3530
+ };
3531
+ });
3524
3532
  build2.onLoad({ filter: /edge-play\/src\/routes\/root\.html$/ }, async (args) => {
3525
3533
  const fs2 = await import("fs/promises");
3526
3534
  const text = await fs2.readFile(args.path, "utf8");
@@ -3829,7 +3837,7 @@ import { join as join8 } from "path";
3829
3837
  // package.json
3830
3838
  var package_default2 = {
3831
3839
  name: "playcademy",
3832
- version: "0.14.11",
3840
+ version: "0.14.12",
3833
3841
  type: "module",
3834
3842
  exports: {
3835
3843
  ".": {
package/dist/version.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // package.json
2
2
  var package_default = {
3
3
  name: "playcademy",
4
- version: "0.14.11",
4
+ version: "0.14.12",
5
5
  type: "module",
6
6
  exports: {
7
7
  ".": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playcademy",
3
- "version": "0.14.12",
3
+ "version": "0.14.13",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {