@xgamesfun/xcore 0.1.0 → 0.2.1
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/LICENSE +1 -1
- package/README.md +183 -84
- package/bin/xcore.mjs +13 -8
- package/commands/add.mjs +11 -8
- package/commands/adopt.mjs +9 -7
- package/commands/backend.mjs +2 -2
- package/commands/check.mjs +4 -4
- package/commands/db.mjs +218 -0
- package/commands/dev.mjs +8 -6
- package/commands/domain.mjs +8 -8
- package/commands/embed.mjs +3 -3
- package/commands/files.mjs +106 -0
- package/commands/login.mjs +9 -17
- package/commands/logout.mjs +15 -4
- package/commands/loot.mjs +2 -2
- package/commands/plan.mjs +22 -19
- package/commands/publish.mjs +54 -21
- package/commands/secret.mjs +6 -6
- package/docs/SERVER.md +218 -18
- package/lib/add.mjs +265 -40
- package/lib/browser.mjs +11 -2
- package/lib/check.mjs +10 -9
- package/lib/cli.mjs +24 -9
- package/lib/client/gamesfun.js +242 -122
- package/lib/client/godot/gamesfun.gd +17 -11
- package/lib/client/godot/gamesfun_peer.gd +2 -2
- package/lib/client/server/0001_init.sql +10 -0
- package/lib/client/server/database.js +271 -109
- package/lib/client/server/gf-db.js +55 -0
- package/lib/client/server/gf-docs.js +56 -0
- package/lib/client/server/rooms.js +16 -9
- package/lib/client/server/rpc.js +7 -7
- package/lib/client/server/stock.js +37 -0
- package/lib/client/server/vouchers.js +127 -46
- package/lib/client/unity/GamesFun.cs +15 -12
- package/lib/client/unity/GamesFun.jslib +1 -1
- package/lib/config-dir.mjs +1 -1
- package/lib/credentials.mjs +10 -5
- package/lib/db.mjs +355 -0
- package/lib/dev/d1.mjs +486 -0
- package/lib/dev/files.mjs +247 -0
- package/lib/dev/gf-local.js +69 -3
- package/lib/dev/index.mjs +68 -20
- package/lib/dev/limits.mjs +2 -2
- package/lib/dev/platform.mjs +111 -12
- package/lib/dev/server.mjs +54 -5
- package/lib/dev/tools.mjs +71 -18
- package/lib/files.mjs +208 -0
- package/lib/open.mjs +1 -1
- package/lib/play-policy.mjs +14 -5
- package/lib/publish/adopt.mjs +73 -33
- package/lib/publish/client.mjs +130 -20
- package/lib/publish/gate.mjs +4 -2
- package/lib/publish/hosting.mjs +5 -5
- package/lib/publish/index.mjs +162 -40
- package/lib/publish/oidc.mjs +26 -5
- package/lib/publish/page.mjs +1 -1
- package/lib/publish/plans.mjs +19 -12
- package/lib/publish/scan.mjs +78 -102
- package/lib/publish/secrets.mjs +4 -5
- package/lib/publish/site.mjs +9 -10
- package/lib/publish/tree.mjs +8 -8
- package/lib/secret-patterns.mjs +53 -16
- package/lib/secret-store.mjs +2 -2
- package/lib/server/bundle.mjs +113 -13
- package/lib/server/contract.mjs +440 -16
- package/lib/server/data-calls.mjs +136 -0
- package/lib/server/secrets.mjs +9 -7
- package/lib/solana-rpc.mjs +5 -5
- package/lib/state-folder.mjs +23 -0
- package/package.json +4 -4
- package/skills/bring-your-own-game/SKILL.md +10 -9
package/lib/server/bundle.mjs
CHANGED
|
@@ -8,18 +8,30 @@
|
|
|
8
8
|
// importScripts (a Worker refuses them anyway; here the person reads where);
|
|
9
9
|
// - no secrets: the publish scan (../publish/scan.mjs) over each server file and over the bundle;
|
|
10
10
|
// - a default export (the Worker), and each room class gamesfun.json declares exported by that name;
|
|
11
|
-
// - the size:
|
|
11
|
+
// - the size: xgames.fun's cap (5 MB, the wasm included), with a word above 1 MB;
|
|
12
12
|
// - a compiled WebAssembly file (`import wasm from '../vendor/x.wasm'`: a physics engine, a codec) is not bundled into
|
|
13
13
|
// the JS: it goes up as its own module (a Worker compiles no wasm from bytes at run time, only a module shipped with
|
|
14
14
|
// it), named by its file name (unique), and the bundle imports it as `./<name>.wasm` (contract §3: up to 16 modules).
|
|
15
|
-
// The esbuild options are the contract's (§3), minified as publish sends it: the same input gives
|
|
15
|
+
// The esbuild options are the contract's (§3), minified as publish sends it: the same input gives xgames.fun's bytes.
|
|
16
16
|
// esbuild is this package's own dependency (package.json); without it the answer says why.
|
|
17
17
|
import { createHash } from 'node:crypto';
|
|
18
18
|
import { readFile } from 'node:fs/promises';
|
|
19
19
|
import path from 'node:path';
|
|
20
20
|
import { scanFile } from '../publish/scan.mjs';
|
|
21
21
|
import { createRequire } from 'node:module';
|
|
22
|
-
import {
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import { CMD } from '../command.mjs';
|
|
24
|
+
import { codeOnly, dataCallLine, ownerlessDataCalls } from './data-calls.mjs';
|
|
25
|
+
import {
|
|
26
|
+
CONFIG_FILE,
|
|
27
|
+
LIMITS,
|
|
28
|
+
NODE_REFUSED,
|
|
29
|
+
OLD_DEFAULT_RULES,
|
|
30
|
+
OWNER_SPACES,
|
|
31
|
+
readServerConfig,
|
|
32
|
+
BUNDLE_NAME,
|
|
33
|
+
usesFiles,
|
|
34
|
+
} from './contract.mjs';
|
|
23
35
|
|
|
24
36
|
const NODE_BUILTINS = new Set([
|
|
25
37
|
'assert',
|
|
@@ -79,14 +91,6 @@ export const FORBIDDEN = [
|
|
|
79
91
|
|
|
80
92
|
const sha256 = b => createHash('sha256').update(b).digest('hex');
|
|
81
93
|
const lineAt = (text, i) => text.slice(0, i).split('\n').length;
|
|
82
|
-
/** Comments and strings blanked (same length), so a word in them is not code. */
|
|
83
|
-
function codeOnly(src) {
|
|
84
|
-
// a string keeps its quotes (so `import('./x.js')` still reads as a literal path), its text becomes spaces
|
|
85
|
-
return src.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*|'(?:\\.|[^'\\\n])*'|"(?:\\.|[^"\\\n])*"/g, m =>
|
|
86
|
-
m[0] === '/' ? m.replace(/[^\n]/g, ' ') : m[0] + ' '.repeat(m.length - 2) + m[0],
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
94
|
let esbuildOnce = null;
|
|
91
95
|
/** esbuild, this package's dependency. Throws with words (code NO_ESBUILD) when it cannot be had. */
|
|
92
96
|
export async function loadEsbuild() {
|
|
@@ -103,7 +107,7 @@ export async function loadEsbuild() {
|
|
|
103
107
|
}
|
|
104
108
|
}
|
|
105
109
|
|
|
106
|
-
/** esbuild's options for a game's server: the contract's (§3, as
|
|
110
|
+
/** esbuild's options for a game's server: the contract's (§3, as xgames.fun bundles it), so a bundle is the same bytes. */
|
|
107
111
|
export function buildOptions(entry, { minify = true, nodeCompat = false } = {}) {
|
|
108
112
|
return {
|
|
109
113
|
entryPoints: [entry],
|
|
@@ -135,9 +139,11 @@ export async function bundleServer(dir, { limits = LIMITS, minify = true } = {})
|
|
|
135
139
|
const out = {
|
|
136
140
|
has: true,
|
|
137
141
|
ok: false,
|
|
142
|
+
stock: cfg.stock,
|
|
138
143
|
entry: cfg.entry,
|
|
139
144
|
objects: cfg.objects,
|
|
140
145
|
compatibilityFlags: cfg.compatibilityFlags,
|
|
146
|
+
database: cfg.database,
|
|
141
147
|
code: null,
|
|
142
148
|
sha256: null,
|
|
143
149
|
bytes: 0,
|
|
@@ -146,10 +152,11 @@ export async function bundleServer(dir, { limits = LIMITS, minify = true } = {})
|
|
|
146
152
|
sources: [],
|
|
147
153
|
problems: [...cfg.problems],
|
|
148
154
|
hits: [],
|
|
149
|
-
warnings: [],
|
|
155
|
+
warnings: [...cfg.warnings],
|
|
150
156
|
};
|
|
151
157
|
if (out.problems.length) return out;
|
|
152
158
|
const { build } = await loadEsbuild();
|
|
159
|
+
if (cfg.stock) return bundleStock(build, out, { minify });
|
|
153
160
|
const refusedImports = [];
|
|
154
161
|
// compiled wasm: its own module, never inlined (the runtime can't compile wasm from bytes)
|
|
155
162
|
const wasmFiles = new Map(); // name → absolute path
|
|
@@ -229,6 +236,10 @@ export async function bundleServer(dir, { limits = LIMITS, minify = true } = {})
|
|
|
229
236
|
}
|
|
230
237
|
if (out.inputs.length > limits.sourceFiles)
|
|
231
238
|
out.problems.push(`server/ is made of ${out.inputs.length} files; at most ${limits.sourceFiles}`);
|
|
239
|
+
// who answers api/db/ (the page's gf.db): server/gf-docs.js's handleDocs, the older database's handleDatabase, or a
|
|
240
|
+
// route of the person's own; the files that define those are not routes themselves
|
|
241
|
+
let routesDocs = false,
|
|
242
|
+
usesDocs = false;
|
|
232
243
|
for (const rel of out.inputs) {
|
|
233
244
|
let bytes;
|
|
234
245
|
try {
|
|
@@ -239,6 +250,16 @@ export async function bundleServer(dir, { limits = LIMITS, minify = true } = {})
|
|
|
239
250
|
if (rel.startsWith('server/')) out.sources.push({ path: rel, sha256: sha256(bytes), bytes: bytes.length });
|
|
240
251
|
if (/\.(m?js|cjs|ts|mts|jsx|tsx)$/.test(rel)) {
|
|
241
252
|
const src = codeOnly(bytes.toString('utf8'));
|
|
253
|
+
// env.GF.files: the game's project files, which xgames.fun must run (server.features "files")
|
|
254
|
+
if (rel.startsWith('server/') && usesFiles(src)) out.usesFiles = true;
|
|
255
|
+
if (rel.startsWith('server/') && rel !== 'server/gf-docs.js' && rel !== 'server/database.js') {
|
|
256
|
+
if (/\bhandleDocs\s*\(/.test(src)) usesDocs = routesDocs = true;
|
|
257
|
+
else if (
|
|
258
|
+
/\b(?:handleDatabase|routeDatabase)\s*\(/.test(src) ||
|
|
259
|
+
/api\/db\b/.test(bytes.toString('utf8').replace(/\/\*[\s\S]*?\*\/|(?<![:'"`])\/\/[^\n]*/g, ''))
|
|
260
|
+
)
|
|
261
|
+
routesDocs = true;
|
|
262
|
+
}
|
|
242
263
|
for (const [what, re] of FORBIDDEN) {
|
|
243
264
|
const m = re.exec(src);
|
|
244
265
|
if (m)
|
|
@@ -314,6 +335,85 @@ export async function bundleServer(dir, { limits = LIMITS, minify = true } = {})
|
|
|
314
335
|
out.warnings.push(
|
|
315
336
|
`The server bundle is ${(out.bytes / 1048576).toFixed(1)} MB: it works, but a smaller server starts faster`,
|
|
316
337
|
);
|
|
338
|
+
// gamesfun.json → database.collections: the page's documents, which a server of its own must answer at api/db/
|
|
339
|
+
if (cfg.database?.has && cfg.database.collections) {
|
|
340
|
+
if (!routesDocs) {
|
|
341
|
+
out.docsUnrouted = true;
|
|
342
|
+
out.warnings.push(
|
|
343
|
+
`${CONFIG_FILE} → database.collections says who may read and write the page's documents (gf.db, at api/db/), and this game's own server does not route api/db/: every gf.db call answers 404, and publish refuses the game. ${CMD} add database writes server/gf-docs.js; route it first thing in fetch: const db = await handleDocs(request, env); if (db) return db; (import { handleDocs } from './gf-docs.js')`,
|
|
344
|
+
);
|
|
345
|
+
} else if (!usesDocs)
|
|
346
|
+
out.warnings.push(
|
|
347
|
+
`${CONFIG_FILE} → database.collections is read by server/gf-docs.js (handleDocs), which this server does not route: its own api/db/ route decides who may read and write`,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
// the database a Durable Object holds (xcore add database before D1): it still works, and the new way is D1
|
|
351
|
+
if (out.inputs.includes('server/database.js') && cfg.objects.some(o => o.class === 'GameData')) {
|
|
352
|
+
const text = await readFile(path.join(root, 'server', 'database.js'), 'utf8').catch(() => '');
|
|
353
|
+
// its RULES as xcore 0.1.0 wrote them, never changed: open to anyone, which is not "working"
|
|
354
|
+
// 0.1.0's code over the database as it is now: a dataOf(env) call with no owner means the server's own space
|
|
355
|
+
if (text.includes(OWNER_SPACES)) {
|
|
356
|
+
const calls = await ownerlessDataCalls(root, out.inputs);
|
|
357
|
+
if (calls.length)
|
|
358
|
+
out.warnings.push(
|
|
359
|
+
`${calls.length === 1 ? 'A call' : `${calls.length} calls`} to dataOf(env) with no owner: since xcore 0.2.0 that is the server's own space, not the document a player wrote (in 0.1.0 a document was found by its id alone). Say whose space each means:\n${calls
|
|
360
|
+
.slice(0, 20)
|
|
361
|
+
.map(c => ` ${dataCallLine(c)}`)
|
|
362
|
+
.join('\n')}${calls.length > 20 ? `\n and ${calls.length - 20} more` : ''}`,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
if (text.includes(OLD_DEFAULT_RULES))
|
|
366
|
+
out.warnings.push(
|
|
367
|
+
`server/database.js still has the RULES xcore 0.1.0 wrote ('*': { read: 'anyone' }): every document in every collection is open to anyone, signed in or not, including what players keep for themselves. ${CMD} add database makes each player's documents their own (read: 'owner'); name a collection that is meant to be public in RULES (levels: { read: 'anyone', write: 'owner' })`,
|
|
368
|
+
);
|
|
369
|
+
else
|
|
370
|
+
out.warnings.push(
|
|
371
|
+
`server/database.js is the older database (a Durable Object, GameData). It keeps working; a new game's database is its own D1 (gamesfun.json → database: ${CMD} add database in a game without it, docs/SERVER.md "A database of its own")`,
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
out.ok = !out.problems.length && !out.hits.length;
|
|
375
|
+
return out;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const STOCK = fileURLToPath(new URL('../client/server/stock.js', import.meta.url));
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* The stock server (../client/server/stock.js) as the game's bundle: a game with a database and no server/ of its
|
|
382
|
+
* own. Its settings (the binding's name and the collections' rules) go in as generated data, the module gf:stock.
|
|
383
|
+
* Nothing of the game folder is read. → bundleServer's answer
|
|
384
|
+
*/
|
|
385
|
+
async function bundleStock(build, out, { minify }) {
|
|
386
|
+
const settings = { binding: out.database.binding, rules: out.database.collections };
|
|
387
|
+
const generated = {
|
|
388
|
+
name: 'gf-stock',
|
|
389
|
+
setup(b) {
|
|
390
|
+
b.onResolve({ filter: /^gf:stock$/ }, () => ({ path: 'gf:stock', namespace: 'gf-stock' }));
|
|
391
|
+
b.onLoad({ filter: /.*/, namespace: 'gf-stock' }, () => ({
|
|
392
|
+
contents: `export default ${JSON.stringify(settings)};`,
|
|
393
|
+
loader: 'js',
|
|
394
|
+
}));
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
let result;
|
|
398
|
+
try {
|
|
399
|
+
result = await build({
|
|
400
|
+
...buildOptions(STOCK, { minify }),
|
|
401
|
+
absWorkingDir: path.dirname(STOCK),
|
|
402
|
+
plugins: [generated],
|
|
403
|
+
});
|
|
404
|
+
} catch (e) {
|
|
405
|
+
for (const m of e.errors ?? [{ text: e.message }]) out.problems.push(`the stock server does not build: ${m.text}`);
|
|
406
|
+
return out;
|
|
407
|
+
}
|
|
408
|
+
const file = result.outputFiles[0];
|
|
409
|
+
out.code = file.contents;
|
|
410
|
+
out.bytes = file.contents.byteLength;
|
|
411
|
+
out.sha256 = sha256(file.contents);
|
|
412
|
+
out.wasm = [];
|
|
413
|
+
const meta =
|
|
414
|
+
Object.values(result.metafile.outputs).find(o => o.entryPoint) ?? Object.values(result.metafile.outputs)[0];
|
|
415
|
+
out.exports = meta?.exports ?? [];
|
|
416
|
+
out.hits.push(...scanFile(`server bundle (${BUNDLE_NAME})`, file.contents));
|
|
317
417
|
out.ok = !out.problems.length && !out.hits.length;
|
|
318
418
|
return out;
|
|
319
419
|
}
|