@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.
Files changed (72) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +183 -84
  3. package/bin/xcore.mjs +13 -8
  4. package/commands/add.mjs +11 -8
  5. package/commands/adopt.mjs +9 -7
  6. package/commands/backend.mjs +2 -2
  7. package/commands/check.mjs +4 -4
  8. package/commands/db.mjs +218 -0
  9. package/commands/dev.mjs +8 -6
  10. package/commands/domain.mjs +8 -8
  11. package/commands/embed.mjs +3 -3
  12. package/commands/files.mjs +106 -0
  13. package/commands/login.mjs +9 -17
  14. package/commands/logout.mjs +15 -4
  15. package/commands/loot.mjs +2 -2
  16. package/commands/plan.mjs +22 -19
  17. package/commands/publish.mjs +54 -21
  18. package/commands/secret.mjs +6 -6
  19. package/docs/SERVER.md +218 -18
  20. package/lib/add.mjs +265 -40
  21. package/lib/browser.mjs +11 -2
  22. package/lib/check.mjs +10 -9
  23. package/lib/cli.mjs +24 -9
  24. package/lib/client/gamesfun.js +242 -122
  25. package/lib/client/godot/gamesfun.gd +17 -11
  26. package/lib/client/godot/gamesfun_peer.gd +2 -2
  27. package/lib/client/server/0001_init.sql +10 -0
  28. package/lib/client/server/database.js +271 -109
  29. package/lib/client/server/gf-db.js +55 -0
  30. package/lib/client/server/gf-docs.js +56 -0
  31. package/lib/client/server/rooms.js +16 -9
  32. package/lib/client/server/rpc.js +7 -7
  33. package/lib/client/server/stock.js +37 -0
  34. package/lib/client/server/vouchers.js +127 -46
  35. package/lib/client/unity/GamesFun.cs +15 -12
  36. package/lib/client/unity/GamesFun.jslib +1 -1
  37. package/lib/config-dir.mjs +1 -1
  38. package/lib/credentials.mjs +10 -5
  39. package/lib/db.mjs +355 -0
  40. package/lib/dev/d1.mjs +486 -0
  41. package/lib/dev/files.mjs +247 -0
  42. package/lib/dev/gf-local.js +69 -3
  43. package/lib/dev/index.mjs +68 -20
  44. package/lib/dev/limits.mjs +2 -2
  45. package/lib/dev/platform.mjs +111 -12
  46. package/lib/dev/server.mjs +54 -5
  47. package/lib/dev/tools.mjs +71 -18
  48. package/lib/files.mjs +208 -0
  49. package/lib/open.mjs +1 -1
  50. package/lib/play-policy.mjs +14 -5
  51. package/lib/publish/adopt.mjs +73 -33
  52. package/lib/publish/client.mjs +130 -20
  53. package/lib/publish/gate.mjs +4 -2
  54. package/lib/publish/hosting.mjs +5 -5
  55. package/lib/publish/index.mjs +162 -40
  56. package/lib/publish/oidc.mjs +26 -5
  57. package/lib/publish/page.mjs +1 -1
  58. package/lib/publish/plans.mjs +19 -12
  59. package/lib/publish/scan.mjs +78 -102
  60. package/lib/publish/secrets.mjs +4 -5
  61. package/lib/publish/site.mjs +9 -10
  62. package/lib/publish/tree.mjs +8 -8
  63. package/lib/secret-patterns.mjs +53 -16
  64. package/lib/secret-store.mjs +2 -2
  65. package/lib/server/bundle.mjs +113 -13
  66. package/lib/server/contract.mjs +440 -16
  67. package/lib/server/data-calls.mjs +136 -0
  68. package/lib/server/secrets.mjs +9 -7
  69. package/lib/solana-rpc.mjs +5 -5
  70. package/lib/state-folder.mjs +23 -0
  71. package/package.json +4 -4
  72. package/skills/bring-your-own-game/SKILL.md +10 -9
@@ -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: games.fun's cap (5 MB, the wasm included), with a word above 1 MB;
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 games.fun's bytes.
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 { LIMITS, NODE_REFUSED, readServerConfig, BUNDLE_NAME } from './contract.mjs';
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 games.fun bundles it), so a bundle is the same bytes. */
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
  }