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 +18 -9
- package/dist/db.js +17 -2
- package/dist/edge-play/src/entry/middleware.ts +3 -2
- package/dist/edge-play/src/stub-entry.ts +140 -0
- package/dist/index.js +114 -23
- package/dist/templates/api/sample-database.ts.template +1 -1
- package/dist/templates/auth/auth.ts.template +13 -1
- package/dist/utils.js +9 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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 () =>
|
|
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
|
-
|
|
181
|
-
|
|
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
|
|
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
|
|
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
|
|
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 () =>
|
|
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(
|
|
6035
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
6166
|
-
const result = await
|
|
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
|
|
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
|
|
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:
|
|
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 =
|
|
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 =
|
|
6665
|
-
const 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
|
-
|
|
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.
|
|
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,
|
|
@@ -11,10 +11,20 @@ import { playcademy } from '@playcademy/better-auth/server'
|
|
|
11
11
|
|
|
12
12
|
import { getDb } from '../../db'
|
|
13
13
|
|
|
14
|
-
|
|
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.
|
|
3840
|
+
version: "0.14.12",
|
|
3833
3841
|
type: "module",
|
|
3834
3842
|
exports: {
|
|
3835
3843
|
".": {
|
package/dist/version.js
CHANGED