hbengine 0.2.3

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 (561) hide show
  1. package/README.md +20 -0
  2. package/engine-cli/connect.mjs +73 -0
  3. package/engine-cli/hbengine.mjs +565 -0
  4. package/engine-cli/package.json +1 -0
  5. package/engine-cli/templates.mjs +14 -0
  6. package/engine-conformance/analyse-worker.mjs +12 -0
  7. package/engine-conformance/analyse.mjs +4458 -0
  8. package/engine-conformance/bundle.mjs +208 -0
  9. package/engine-conformance/clock.mjs +294 -0
  10. package/engine-conformance/csp.go +56 -0
  11. package/engine-conformance/csp.json +6 -0
  12. package/engine-conformance/csp.mjs +20 -0
  13. package/engine-conformance/gzip-sizes.mjs +9 -0
  14. package/engine-conformance/package.json +1 -0
  15. package/engine-conformance/picture.mjs +55 -0
  16. package/engine-conformance/report.go +349 -0
  17. package/engine-conformance/run.mjs +1217 -0
  18. package/engine-conformance/scan.mjs +476 -0
  19. package/engine-conformance/testdata/clock-overlay/bundle/assets.json +1 -0
  20. package/engine-conformance/testdata/clock-overlay/bundle/game.css +17 -0
  21. package/engine-conformance/testdata/clock-overlay/bundle/game.js +83 -0
  22. package/engine-conformance/testdata/clock-overlay/bundle/hbtest.json +4 -0
  23. package/engine-conformance/testdata/clock-overlay/bundle/index.html +34 -0
  24. package/engine-conformance/testdata/clock-probe/bundle/assets.json +1 -0
  25. package/engine-conformance/testdata/clock-probe/bundle/game.css +17 -0
  26. package/engine-conformance/testdata/clock-probe/bundle/game.js +65 -0
  27. package/engine-conformance/testdata/clock-probe/bundle/hbtest.json +4 -0
  28. package/engine-conformance/testdata/clock-probe/bundle/index.html +35 -0
  29. package/engine-conformance/testdata/clock-probe/bundle/probe-worker.js +46 -0
  30. package/engine-conformance/testdata/clock-probe/bundle/probe.js +210 -0
  31. package/engine-conformance/testdata/clock-probe/math/dice.tsv +100 -0
  32. package/engine-conformance/testdata/clock-probe/math/manifest.json +2925 -0
  33. package/engine-conformance/testdata/clock-worker/bundle/assets.json +1 -0
  34. package/engine-conformance/testdata/clock-worker/bundle/clock-worker.js +14 -0
  35. package/engine-conformance/testdata/clock-worker/bundle/game.css +17 -0
  36. package/engine-conformance/testdata/clock-worker/bundle/game.js +61 -0
  37. package/engine-conformance/testdata/clock-worker/bundle/hbtest.json +4 -0
  38. package/engine-conformance/testdata/clock-worker/bundle/index.html +35 -0
  39. package/engine-conformance/testdata/clock-worker/math/dice.tsv +100 -0
  40. package/engine-conformance/testdata/clock-worker/math/manifest.json +2925 -0
  41. package/engine-conformance/travel.mjs +1209 -0
  42. package/engine-conformance/vendor/acorn/LICENSE +21 -0
  43. package/engine-conformance/vendor/acorn/acorn.mjs +6266 -0
  44. package/engine-conformance/wasm-releases.json +168 -0
  45. package/engine-examples/README.md +23 -0
  46. package/engine-examples/coin/AGENTS.md +3 -0
  47. package/engine-examples/coin/README.md +7 -0
  48. package/engine-examples/coin/bundle/assets.json +1 -0
  49. package/engine-examples/coin/bundle/game.css +5 -0
  50. package/engine-examples/coin/bundle/game.js +19 -0
  51. package/engine-examples/coin/bundle/hbtest.json +5 -0
  52. package/engine-examples/coin/bundle/index.html +16 -0
  53. package/engine-examples/coin/math/coin.tsv +2 -0
  54. package/engine-examples/coin/math/manifest.json +17 -0
  55. package/engine-examples/crash/AGENTS.md +3 -0
  56. package/engine-examples/crash/README.md +14 -0
  57. package/engine-examples/crash/bundle/assets.json +1 -0
  58. package/engine-examples/crash/bundle/game.css +8 -0
  59. package/engine-examples/crash/bundle/game.js +90 -0
  60. package/engine-examples/crash/bundle/hbtest.json +5 -0
  61. package/engine-examples/crash/bundle/index.html +20 -0
  62. package/engine-examples/crash/math/crash-01.tsv +2 -0
  63. package/engine-examples/crash/math/crash-02.tsv +2 -0
  64. package/engine-examples/crash/math/crash-03.tsv +2 -0
  65. package/engine-examples/crash/math/crash-04.tsv +2 -0
  66. package/engine-examples/crash/math/crash-05.tsv +2 -0
  67. package/engine-examples/crash/math/crash-06.tsv +2 -0
  68. package/engine-examples/crash/math/crash-07.tsv +2 -0
  69. package/engine-examples/crash/math/crash-08.tsv +2 -0
  70. package/engine-examples/crash/math/crash-09.tsv +2 -0
  71. package/engine-examples/crash/math/crash-10.tsv +2 -0
  72. package/engine-examples/crash/math/crash-11.tsv +2 -0
  73. package/engine-examples/crash/math/crash-12.tsv +2 -0
  74. package/engine-examples/crash/math/crash-13.tsv +2 -0
  75. package/engine-examples/crash/math/crash-14.tsv +2 -0
  76. package/engine-examples/crash/math/crash-15.tsv +2 -0
  77. package/engine-examples/crash/math/crash-16.tsv +2 -0
  78. package/engine-examples/crash/math/crash-17.tsv +2 -0
  79. package/engine-examples/crash/math/crash-18.tsv +2 -0
  80. package/engine-examples/crash/math/crash-19.tsv +2 -0
  81. package/engine-examples/crash/math/crash-20.tsv +2 -0
  82. package/engine-examples/crash/math/crash-21.tsv +2 -0
  83. package/engine-examples/crash/math/crash-22.tsv +2 -0
  84. package/engine-examples/crash/math/crash-23.tsv +2 -0
  85. package/engine-examples/crash/math/crash-24.tsv +2 -0
  86. package/engine-examples/crash/math/crash-25.tsv +2 -0
  87. package/engine-examples/crash/math/crash-26.tsv +2 -0
  88. package/engine-examples/crash/math/crash-27.tsv +2 -0
  89. package/engine-examples/crash/math/crash-28.tsv +2 -0
  90. package/engine-examples/crash/math/crash-29.tsv +2 -0
  91. package/engine-examples/crash/math/crash-30.tsv +2 -0
  92. package/engine-examples/crash/math/crash-31.tsv +2 -0
  93. package/engine-examples/crash/math/crash-32.tsv +2 -0
  94. package/engine-examples/crash/math/crash-33.tsv +2 -0
  95. package/engine-examples/crash/math/crash-34.tsv +2 -0
  96. package/engine-examples/crash/math/crash-35.tsv +2 -0
  97. package/engine-examples/crash/math/crash-36.tsv +2 -0
  98. package/engine-examples/crash/math/crash-37.tsv +2 -0
  99. package/engine-examples/crash/math/crash-38.tsv +2 -0
  100. package/engine-examples/crash/math/crash-39.tsv +2 -0
  101. package/engine-examples/crash/math/crash-40.tsv +2 -0
  102. package/engine-examples/crash/math/crash-41.tsv +2 -0
  103. package/engine-examples/crash/math/crash-42.tsv +2 -0
  104. package/engine-examples/crash/math/crash-43.tsv +2 -0
  105. package/engine-examples/crash/math/crash-44.tsv +2 -0
  106. package/engine-examples/crash/math/crash-45.tsv +2 -0
  107. package/engine-examples/crash/math/crash-46.tsv +2 -0
  108. package/engine-examples/crash/math/crash-47.tsv +2 -0
  109. package/engine-examples/crash/math/crash-48.tsv +2 -0
  110. package/engine-examples/crash/math/crash-49.tsv +2 -0
  111. package/engine-examples/crash/math/crash-50.tsv +2 -0
  112. package/engine-examples/crash/math/crash-51.tsv +2 -0
  113. package/engine-examples/crash/math/crash-52.tsv +2 -0
  114. package/engine-examples/crash/math/crash-53.tsv +2 -0
  115. package/engine-examples/crash/math/crash-54.tsv +2 -0
  116. package/engine-examples/crash/math/crash-55.tsv +2 -0
  117. package/engine-examples/crash/math/crash-56.tsv +2 -0
  118. package/engine-examples/crash/math/crash-57.tsv +2 -0
  119. package/engine-examples/crash/math/crash-58.tsv +2 -0
  120. package/engine-examples/crash/math/crash-59.tsv +2 -0
  121. package/engine-examples/crash/math/crash-60.tsv +2 -0
  122. package/engine-examples/crash/math/crash-61.tsv +2 -0
  123. package/engine-examples/crash/math/crash-62.tsv +2 -0
  124. package/engine-examples/crash/math/crash-63.tsv +2 -0
  125. package/engine-examples/crash/math/crash-64.tsv +2 -0
  126. package/engine-examples/crash/math/gen.mjs +31 -0
  127. package/engine-examples/crash/math/manifest.json +1 -0
  128. package/engine-examples/dice/AGENTS.md +3 -0
  129. package/engine-examples/dice/README.md +25 -0
  130. package/engine-examples/dice/bundle/assets.json +1 -0
  131. package/engine-examples/dice/bundle/game.css +17 -0
  132. package/engine-examples/dice/bundle/game.js +61 -0
  133. package/engine-examples/dice/bundle/hbtest.json +4 -0
  134. package/engine-examples/dice/bundle/index.html +34 -0
  135. package/engine-examples/dice/math/dice.tsv +100 -0
  136. package/engine-examples/dice/math/manifest.json +2925 -0
  137. package/engine-examples/keno/AGENTS.md +3 -0
  138. package/engine-examples/keno/README.md +11 -0
  139. package/engine-examples/keno/bundle/assets.json +1 -0
  140. package/engine-examples/keno/bundle/game.css +14 -0
  141. package/engine-examples/keno/bundle/game.js +86 -0
  142. package/engine-examples/keno/bundle/hbtest.json +4 -0
  143. package/engine-examples/keno/bundle/i18n.json +342 -0
  144. package/engine-examples/keno/bundle/index.html +20 -0
  145. package/engine-examples/keno/bundle/model.json +1 -0
  146. package/engine-examples/keno/math/gen.mjs +35 -0
  147. package/engine-examples/keno/math/keno-picks-1.tsv +2 -0
  148. package/engine-examples/keno/math/keno-picks-10.tsv +11 -0
  149. package/engine-examples/keno/math/keno-picks-2.tsv +3 -0
  150. package/engine-examples/keno/math/keno-picks-3.tsv +4 -0
  151. package/engine-examples/keno/math/keno-picks-4.tsv +5 -0
  152. package/engine-examples/keno/math/keno-picks-5.tsv +6 -0
  153. package/engine-examples/keno/math/keno-picks-6.tsv +7 -0
  154. package/engine-examples/keno/math/keno-picks-7.tsv +8 -0
  155. package/engine-examples/keno/math/keno-picks-8.tsv +9 -0
  156. package/engine-examples/keno/math/keno-picks-9.tsv +10 -0
  157. package/engine-examples/keno/math/manifest.json +1 -0
  158. package/engine-examples/keno/math-kit/pack.mjs +165 -0
  159. package/engine-examples/keno/math-kit/package.json +8 -0
  160. package/engine-examples/keno/math-kit/slot.mjs +94 -0
  161. package/engine-examples/keno/math-kit/tumble.mjs +94 -0
  162. package/engine-examples/lines/AGENTS.md +3 -0
  163. package/engine-examples/lines/README.md +20 -0
  164. package/engine-examples/lines/bundle/assets.json +1 -0
  165. package/engine-examples/lines/bundle/game.css +16 -0
  166. package/engine-examples/lines/bundle/game.js +125 -0
  167. package/engine-examples/lines/bundle/hbtest.json +4 -0
  168. package/engine-examples/lines/bundle/i18n.json +242 -0
  169. package/engine-examples/lines/bundle/index.html +23 -0
  170. package/engine-examples/lines/bundle/model.json +1 -0
  171. package/engine-examples/lines/math/gen.mjs +54 -0
  172. package/engine-examples/lines/math-kit/pack.mjs +165 -0
  173. package/engine-examples/lines/math-kit/package.json +8 -0
  174. package/engine-examples/lines/math-kit/slot.mjs +94 -0
  175. package/engine-examples/lines/math-kit/tumble.mjs +94 -0
  176. package/engine-examples/mines/AGENTS.md +3 -0
  177. package/engine-examples/mines/README.md +13 -0
  178. package/engine-examples/mines/bundle/assets.json +1 -0
  179. package/engine-examples/mines/bundle/game.css +12 -0
  180. package/engine-examples/mines/bundle/game.js +58 -0
  181. package/engine-examples/mines/bundle/hbtest.json +8 -0
  182. package/engine-examples/mines/bundle/index.html +16 -0
  183. package/engine-examples/mines/math/gen.mjs +40 -0
  184. package/engine-examples/mines/math/manifest.json +1 -0
  185. package/engine-examples/mines/math/mines-1-1.tsv +2 -0
  186. package/engine-examples/mines/math/mines-1-2.tsv +2 -0
  187. package/engine-examples/mines/math/mines-1-22.tsv +2 -0
  188. package/engine-examples/mines/math/mines-1-23.tsv +2 -0
  189. package/engine-examples/mines/math/mines-1-24.tsv +2 -0
  190. package/engine-examples/mines/math/mines-1-3.tsv +2 -0
  191. package/engine-examples/mines/math/mines-10-1.tsv +2 -0
  192. package/engine-examples/mines/math/mines-10-10.tsv +2 -0
  193. package/engine-examples/mines/math/mines-10-11.tsv +2 -0
  194. package/engine-examples/mines/math/mines-10-12.tsv +2 -0
  195. package/engine-examples/mines/math/mines-10-13.tsv +2 -0
  196. package/engine-examples/mines/math/mines-10-14.tsv +2 -0
  197. package/engine-examples/mines/math/mines-10-15.tsv +2 -0
  198. package/engine-examples/mines/math/mines-10-2.tsv +2 -0
  199. package/engine-examples/mines/math/mines-10-3.tsv +2 -0
  200. package/engine-examples/mines/math/mines-10-4.tsv +2 -0
  201. package/engine-examples/mines/math/mines-10-5.tsv +2 -0
  202. package/engine-examples/mines/math/mines-10-6.tsv +2 -0
  203. package/engine-examples/mines/math/mines-10-7.tsv +2 -0
  204. package/engine-examples/mines/math/mines-10-8.tsv +2 -0
  205. package/engine-examples/mines/math/mines-10-9.tsv +2 -0
  206. package/engine-examples/mines/math/mines-11-1.tsv +2 -0
  207. package/engine-examples/mines/math/mines-11-10.tsv +2 -0
  208. package/engine-examples/mines/math/mines-11-11.tsv +2 -0
  209. package/engine-examples/mines/math/mines-11-12.tsv +2 -0
  210. package/engine-examples/mines/math/mines-11-13.tsv +2 -0
  211. package/engine-examples/mines/math/mines-11-14.tsv +2 -0
  212. package/engine-examples/mines/math/mines-11-2.tsv +2 -0
  213. package/engine-examples/mines/math/mines-11-3.tsv +2 -0
  214. package/engine-examples/mines/math/mines-11-4.tsv +2 -0
  215. package/engine-examples/mines/math/mines-11-5.tsv +2 -0
  216. package/engine-examples/mines/math/mines-11-6.tsv +2 -0
  217. package/engine-examples/mines/math/mines-11-7.tsv +2 -0
  218. package/engine-examples/mines/math/mines-11-8.tsv +2 -0
  219. package/engine-examples/mines/math/mines-11-9.tsv +2 -0
  220. package/engine-examples/mines/math/mines-12-1.tsv +2 -0
  221. package/engine-examples/mines/math/mines-12-10.tsv +2 -0
  222. package/engine-examples/mines/math/mines-12-11.tsv +2 -0
  223. package/engine-examples/mines/math/mines-12-12.tsv +2 -0
  224. package/engine-examples/mines/math/mines-12-13.tsv +2 -0
  225. package/engine-examples/mines/math/mines-12-2.tsv +2 -0
  226. package/engine-examples/mines/math/mines-12-3.tsv +2 -0
  227. package/engine-examples/mines/math/mines-12-4.tsv +2 -0
  228. package/engine-examples/mines/math/mines-12-5.tsv +2 -0
  229. package/engine-examples/mines/math/mines-12-6.tsv +2 -0
  230. package/engine-examples/mines/math/mines-12-7.tsv +2 -0
  231. package/engine-examples/mines/math/mines-12-8.tsv +2 -0
  232. package/engine-examples/mines/math/mines-12-9.tsv +2 -0
  233. package/engine-examples/mines/math/mines-13-1.tsv +2 -0
  234. package/engine-examples/mines/math/mines-13-10.tsv +2 -0
  235. package/engine-examples/mines/math/mines-13-11.tsv +2 -0
  236. package/engine-examples/mines/math/mines-13-12.tsv +2 -0
  237. package/engine-examples/mines/math/mines-13-2.tsv +2 -0
  238. package/engine-examples/mines/math/mines-13-3.tsv +2 -0
  239. package/engine-examples/mines/math/mines-13-4.tsv +2 -0
  240. package/engine-examples/mines/math/mines-13-5.tsv +2 -0
  241. package/engine-examples/mines/math/mines-13-6.tsv +2 -0
  242. package/engine-examples/mines/math/mines-13-7.tsv +2 -0
  243. package/engine-examples/mines/math/mines-13-8.tsv +2 -0
  244. package/engine-examples/mines/math/mines-13-9.tsv +2 -0
  245. package/engine-examples/mines/math/mines-14-1.tsv +2 -0
  246. package/engine-examples/mines/math/mines-14-10.tsv +2 -0
  247. package/engine-examples/mines/math/mines-14-11.tsv +2 -0
  248. package/engine-examples/mines/math/mines-14-2.tsv +2 -0
  249. package/engine-examples/mines/math/mines-14-3.tsv +2 -0
  250. package/engine-examples/mines/math/mines-14-4.tsv +2 -0
  251. package/engine-examples/mines/math/mines-14-5.tsv +2 -0
  252. package/engine-examples/mines/math/mines-14-6.tsv +2 -0
  253. package/engine-examples/mines/math/mines-14-7.tsv +2 -0
  254. package/engine-examples/mines/math/mines-14-8.tsv +2 -0
  255. package/engine-examples/mines/math/mines-14-9.tsv +2 -0
  256. package/engine-examples/mines/math/mines-15-1.tsv +2 -0
  257. package/engine-examples/mines/math/mines-15-10.tsv +2 -0
  258. package/engine-examples/mines/math/mines-15-2.tsv +2 -0
  259. package/engine-examples/mines/math/mines-15-3.tsv +2 -0
  260. package/engine-examples/mines/math/mines-15-4.tsv +2 -0
  261. package/engine-examples/mines/math/mines-15-5.tsv +2 -0
  262. package/engine-examples/mines/math/mines-15-6.tsv +2 -0
  263. package/engine-examples/mines/math/mines-15-7.tsv +2 -0
  264. package/engine-examples/mines/math/mines-15-8.tsv +2 -0
  265. package/engine-examples/mines/math/mines-15-9.tsv +2 -0
  266. package/engine-examples/mines/math/mines-16-1.tsv +2 -0
  267. package/engine-examples/mines/math/mines-16-2.tsv +2 -0
  268. package/engine-examples/mines/math/mines-16-3.tsv +2 -0
  269. package/engine-examples/mines/math/mines-16-4.tsv +2 -0
  270. package/engine-examples/mines/math/mines-16-5.tsv +2 -0
  271. package/engine-examples/mines/math/mines-16-6.tsv +2 -0
  272. package/engine-examples/mines/math/mines-16-7.tsv +2 -0
  273. package/engine-examples/mines/math/mines-16-8.tsv +2 -0
  274. package/engine-examples/mines/math/mines-16-9.tsv +2 -0
  275. package/engine-examples/mines/math/mines-17-1.tsv +2 -0
  276. package/engine-examples/mines/math/mines-17-2.tsv +2 -0
  277. package/engine-examples/mines/math/mines-17-3.tsv +2 -0
  278. package/engine-examples/mines/math/mines-17-4.tsv +2 -0
  279. package/engine-examples/mines/math/mines-17-5.tsv +2 -0
  280. package/engine-examples/mines/math/mines-17-6.tsv +2 -0
  281. package/engine-examples/mines/math/mines-17-7.tsv +2 -0
  282. package/engine-examples/mines/math/mines-17-8.tsv +2 -0
  283. package/engine-examples/mines/math/mines-18-1.tsv +2 -0
  284. package/engine-examples/mines/math/mines-18-2.tsv +2 -0
  285. package/engine-examples/mines/math/mines-18-3.tsv +2 -0
  286. package/engine-examples/mines/math/mines-18-4.tsv +2 -0
  287. package/engine-examples/mines/math/mines-18-5.tsv +2 -0
  288. package/engine-examples/mines/math/mines-18-6.tsv +2 -0
  289. package/engine-examples/mines/math/mines-18-7.tsv +2 -0
  290. package/engine-examples/mines/math/mines-19-1.tsv +2 -0
  291. package/engine-examples/mines/math/mines-19-2.tsv +2 -0
  292. package/engine-examples/mines/math/mines-19-3.tsv +2 -0
  293. package/engine-examples/mines/math/mines-19-4.tsv +2 -0
  294. package/engine-examples/mines/math/mines-19-5.tsv +2 -0
  295. package/engine-examples/mines/math/mines-19-6.tsv +2 -0
  296. package/engine-examples/mines/math/mines-2-1.tsv +2 -0
  297. package/engine-examples/mines/math/mines-2-19.tsv +2 -0
  298. package/engine-examples/mines/math/mines-2-2.tsv +2 -0
  299. package/engine-examples/mines/math/mines-2-20.tsv +2 -0
  300. package/engine-examples/mines/math/mines-2-21.tsv +2 -0
  301. package/engine-examples/mines/math/mines-2-22.tsv +2 -0
  302. package/engine-examples/mines/math/mines-2-23.tsv +2 -0
  303. package/engine-examples/mines/math/mines-2-3.tsv +2 -0
  304. package/engine-examples/mines/math/mines-2-4.tsv +2 -0
  305. package/engine-examples/mines/math/mines-2-5.tsv +2 -0
  306. package/engine-examples/mines/math/mines-20-1.tsv +2 -0
  307. package/engine-examples/mines/math/mines-20-2.tsv +2 -0
  308. package/engine-examples/mines/math/mines-20-3.tsv +2 -0
  309. package/engine-examples/mines/math/mines-20-4.tsv +2 -0
  310. package/engine-examples/mines/math/mines-20-5.tsv +2 -0
  311. package/engine-examples/mines/math/mines-21-1.tsv +2 -0
  312. package/engine-examples/mines/math/mines-21-2.tsv +2 -0
  313. package/engine-examples/mines/math/mines-21-3.tsv +2 -0
  314. package/engine-examples/mines/math/mines-21-4.tsv +2 -0
  315. package/engine-examples/mines/math/mines-22-1.tsv +2 -0
  316. package/engine-examples/mines/math/mines-22-2.tsv +2 -0
  317. package/engine-examples/mines/math/mines-22-3.tsv +2 -0
  318. package/engine-examples/mines/math/mines-23-1.tsv +2 -0
  319. package/engine-examples/mines/math/mines-23-2.tsv +2 -0
  320. package/engine-examples/mines/math/mines-24-1.tsv +2 -0
  321. package/engine-examples/mines/math/mines-3-1.tsv +2 -0
  322. package/engine-examples/mines/math/mines-3-17.tsv +2 -0
  323. package/engine-examples/mines/math/mines-3-18.tsv +2 -0
  324. package/engine-examples/mines/math/mines-3-19.tsv +2 -0
  325. package/engine-examples/mines/math/mines-3-2.tsv +2 -0
  326. package/engine-examples/mines/math/mines-3-20.tsv +2 -0
  327. package/engine-examples/mines/math/mines-3-21.tsv +2 -0
  328. package/engine-examples/mines/math/mines-3-22.tsv +2 -0
  329. package/engine-examples/mines/math/mines-3-3.tsv +2 -0
  330. package/engine-examples/mines/math/mines-3-4.tsv +2 -0
  331. package/engine-examples/mines/math/mines-3-5.tsv +2 -0
  332. package/engine-examples/mines/math/mines-3-6.tsv +2 -0
  333. package/engine-examples/mines/math/mines-4-1.tsv +2 -0
  334. package/engine-examples/mines/math/mines-4-13.tsv +2 -0
  335. package/engine-examples/mines/math/mines-4-14.tsv +2 -0
  336. package/engine-examples/mines/math/mines-4-15.tsv +2 -0
  337. package/engine-examples/mines/math/mines-4-16.tsv +2 -0
  338. package/engine-examples/mines/math/mines-4-17.tsv +2 -0
  339. package/engine-examples/mines/math/mines-4-18.tsv +2 -0
  340. package/engine-examples/mines/math/mines-4-19.tsv +2 -0
  341. package/engine-examples/mines/math/mines-4-2.tsv +2 -0
  342. package/engine-examples/mines/math/mines-4-20.tsv +2 -0
  343. package/engine-examples/mines/math/mines-4-21.tsv +2 -0
  344. package/engine-examples/mines/math/mines-4-3.tsv +2 -0
  345. package/engine-examples/mines/math/mines-4-4.tsv +2 -0
  346. package/engine-examples/mines/math/mines-4-5.tsv +2 -0
  347. package/engine-examples/mines/math/mines-4-6.tsv +2 -0
  348. package/engine-examples/mines/math/mines-4-7.tsv +2 -0
  349. package/engine-examples/mines/math/mines-4-8.tsv +2 -0
  350. package/engine-examples/mines/math/mines-4-9.tsv +2 -0
  351. package/engine-examples/mines/math/mines-5-1.tsv +2 -0
  352. package/engine-examples/mines/math/mines-5-10.tsv +2 -0
  353. package/engine-examples/mines/math/mines-5-11.tsv +2 -0
  354. package/engine-examples/mines/math/mines-5-12.tsv +2 -0
  355. package/engine-examples/mines/math/mines-5-13.tsv +2 -0
  356. package/engine-examples/mines/math/mines-5-14.tsv +2 -0
  357. package/engine-examples/mines/math/mines-5-15.tsv +2 -0
  358. package/engine-examples/mines/math/mines-5-16.tsv +2 -0
  359. package/engine-examples/mines/math/mines-5-17.tsv +2 -0
  360. package/engine-examples/mines/math/mines-5-18.tsv +2 -0
  361. package/engine-examples/mines/math/mines-5-19.tsv +2 -0
  362. package/engine-examples/mines/math/mines-5-2.tsv +2 -0
  363. package/engine-examples/mines/math/mines-5-20.tsv +2 -0
  364. package/engine-examples/mines/math/mines-5-3.tsv +2 -0
  365. package/engine-examples/mines/math/mines-5-4.tsv +2 -0
  366. package/engine-examples/mines/math/mines-5-5.tsv +2 -0
  367. package/engine-examples/mines/math/mines-5-6.tsv +2 -0
  368. package/engine-examples/mines/math/mines-5-7.tsv +2 -0
  369. package/engine-examples/mines/math/mines-5-8.tsv +2 -0
  370. package/engine-examples/mines/math/mines-5-9.tsv +2 -0
  371. package/engine-examples/mines/math/mines-6-1.tsv +2 -0
  372. package/engine-examples/mines/math/mines-6-10.tsv +2 -0
  373. package/engine-examples/mines/math/mines-6-11.tsv +2 -0
  374. package/engine-examples/mines/math/mines-6-12.tsv +2 -0
  375. package/engine-examples/mines/math/mines-6-13.tsv +2 -0
  376. package/engine-examples/mines/math/mines-6-14.tsv +2 -0
  377. package/engine-examples/mines/math/mines-6-15.tsv +2 -0
  378. package/engine-examples/mines/math/mines-6-16.tsv +2 -0
  379. package/engine-examples/mines/math/mines-6-17.tsv +2 -0
  380. package/engine-examples/mines/math/mines-6-18.tsv +2 -0
  381. package/engine-examples/mines/math/mines-6-19.tsv +2 -0
  382. package/engine-examples/mines/math/mines-6-2.tsv +2 -0
  383. package/engine-examples/mines/math/mines-6-3.tsv +2 -0
  384. package/engine-examples/mines/math/mines-6-4.tsv +2 -0
  385. package/engine-examples/mines/math/mines-6-5.tsv +2 -0
  386. package/engine-examples/mines/math/mines-6-6.tsv +2 -0
  387. package/engine-examples/mines/math/mines-6-7.tsv +2 -0
  388. package/engine-examples/mines/math/mines-6-8.tsv +2 -0
  389. package/engine-examples/mines/math/mines-6-9.tsv +2 -0
  390. package/engine-examples/mines/math/mines-7-1.tsv +2 -0
  391. package/engine-examples/mines/math/mines-7-10.tsv +2 -0
  392. package/engine-examples/mines/math/mines-7-11.tsv +2 -0
  393. package/engine-examples/mines/math/mines-7-12.tsv +2 -0
  394. package/engine-examples/mines/math/mines-7-13.tsv +2 -0
  395. package/engine-examples/mines/math/mines-7-14.tsv +2 -0
  396. package/engine-examples/mines/math/mines-7-15.tsv +2 -0
  397. package/engine-examples/mines/math/mines-7-16.tsv +2 -0
  398. package/engine-examples/mines/math/mines-7-17.tsv +2 -0
  399. package/engine-examples/mines/math/mines-7-18.tsv +2 -0
  400. package/engine-examples/mines/math/mines-7-2.tsv +2 -0
  401. package/engine-examples/mines/math/mines-7-3.tsv +2 -0
  402. package/engine-examples/mines/math/mines-7-4.tsv +2 -0
  403. package/engine-examples/mines/math/mines-7-5.tsv +2 -0
  404. package/engine-examples/mines/math/mines-7-6.tsv +2 -0
  405. package/engine-examples/mines/math/mines-7-7.tsv +2 -0
  406. package/engine-examples/mines/math/mines-7-8.tsv +2 -0
  407. package/engine-examples/mines/math/mines-7-9.tsv +2 -0
  408. package/engine-examples/mines/math/mines-8-1.tsv +2 -0
  409. package/engine-examples/mines/math/mines-8-10.tsv +2 -0
  410. package/engine-examples/mines/math/mines-8-11.tsv +2 -0
  411. package/engine-examples/mines/math/mines-8-12.tsv +2 -0
  412. package/engine-examples/mines/math/mines-8-13.tsv +2 -0
  413. package/engine-examples/mines/math/mines-8-14.tsv +2 -0
  414. package/engine-examples/mines/math/mines-8-15.tsv +2 -0
  415. package/engine-examples/mines/math/mines-8-16.tsv +2 -0
  416. package/engine-examples/mines/math/mines-8-17.tsv +2 -0
  417. package/engine-examples/mines/math/mines-8-2.tsv +2 -0
  418. package/engine-examples/mines/math/mines-8-3.tsv +2 -0
  419. package/engine-examples/mines/math/mines-8-4.tsv +2 -0
  420. package/engine-examples/mines/math/mines-8-5.tsv +2 -0
  421. package/engine-examples/mines/math/mines-8-6.tsv +2 -0
  422. package/engine-examples/mines/math/mines-8-7.tsv +2 -0
  423. package/engine-examples/mines/math/mines-8-8.tsv +2 -0
  424. package/engine-examples/mines/math/mines-8-9.tsv +2 -0
  425. package/engine-examples/mines/math/mines-9-1.tsv +2 -0
  426. package/engine-examples/mines/math/mines-9-10.tsv +2 -0
  427. package/engine-examples/mines/math/mines-9-11.tsv +2 -0
  428. package/engine-examples/mines/math/mines-9-12.tsv +2 -0
  429. package/engine-examples/mines/math/mines-9-13.tsv +2 -0
  430. package/engine-examples/mines/math/mines-9-14.tsv +2 -0
  431. package/engine-examples/mines/math/mines-9-15.tsv +2 -0
  432. package/engine-examples/mines/math/mines-9-16.tsv +2 -0
  433. package/engine-examples/mines/math/mines-9-2.tsv +2 -0
  434. package/engine-examples/mines/math/mines-9-3.tsv +2 -0
  435. package/engine-examples/mines/math/mines-9-4.tsv +2 -0
  436. package/engine-examples/mines/math/mines-9-5.tsv +2 -0
  437. package/engine-examples/mines/math/mines-9-6.tsv +2 -0
  438. package/engine-examples/mines/math/mines-9-7.tsv +2 -0
  439. package/engine-examples/mines/math/mines-9-8.tsv +2 -0
  440. package/engine-examples/mines/math/mines-9-9.tsv +2 -0
  441. package/engine-examples/plinko/AGENTS.md +3 -0
  442. package/engine-examples/plinko/README.md +9 -0
  443. package/engine-examples/plinko/bundle/assets.json +1 -0
  444. package/engine-examples/plinko/bundle/game.css +5 -0
  445. package/engine-examples/plinko/bundle/game.js +92 -0
  446. package/engine-examples/plinko/bundle/hbtest.json +4 -0
  447. package/engine-examples/plinko/bundle/index.html +16 -0
  448. package/engine-examples/plinko/bundle/model.json +1 -0
  449. package/engine-examples/plinko/math/gen.mjs +40 -0
  450. package/engine-examples/plinko/math/manifest.json +1 -0
  451. package/engine-examples/plinko/math/plinko-12-high.tsv +13 -0
  452. package/engine-examples/plinko/math/plinko-12-low.tsv +13 -0
  453. package/engine-examples/plinko/math/plinko-12-medium.tsv +13 -0
  454. package/engine-examples/plinko/math/plinko-16-high.tsv +17 -0
  455. package/engine-examples/plinko/math/plinko-16-low.tsv +17 -0
  456. package/engine-examples/plinko/math/plinko-16-medium.tsv +17 -0
  457. package/engine-examples/plinko/math/plinko-8-high.tsv +9 -0
  458. package/engine-examples/plinko/math/plinko-8-low.tsv +9 -0
  459. package/engine-examples/plinko/math/plinko-8-medium.tsv +9 -0
  460. package/engine-examples/plinko/math-kit/pack.mjs +165 -0
  461. package/engine-examples/plinko/math-kit/package.json +8 -0
  462. package/engine-examples/plinko/math-kit/slot.mjs +94 -0
  463. package/engine-examples/plinko/math-kit/tumble.mjs +94 -0
  464. package/engine-examples/slot/AGENTS.md +3 -0
  465. package/engine-examples/slot/README.md +26 -0
  466. package/engine-examples/slot/bundle/assets.json +1 -0
  467. package/engine-examples/slot/bundle/game.css +19 -0
  468. package/engine-examples/slot/bundle/game.js +42 -0
  469. package/engine-examples/slot/bundle/hbtest.json +4 -0
  470. package/engine-examples/slot/bundle/index.html +23 -0
  471. package/engine-examples/slot/math/gen.mjs +79 -0
  472. package/engine-examples/slot/math/manifest.json +1 -0
  473. package/engine-examples/slot/math/slot.tsv +110592 -0
  474. package/engine-examples/tower/AGENTS.md +3 -0
  475. package/engine-examples/tower/README.md +21 -0
  476. package/engine-examples/tower/bundle/assets.json +1 -0
  477. package/engine-examples/tower/bundle/game.css +8 -0
  478. package/engine-examples/tower/bundle/game.js +37 -0
  479. package/engine-examples/tower/bundle/hbtest.json +5 -0
  480. package/engine-examples/tower/bundle/index.html +16 -0
  481. package/engine-examples/tower/math/floor.tsv +4 -0
  482. package/engine-examples/tower/math/manifest.json +61 -0
  483. package/engine-examples/tumble/AGENTS.md +3 -0
  484. package/engine-examples/tumble/README.md +18 -0
  485. package/engine-examples/tumble/bundle/assets.json +1 -0
  486. package/engine-examples/tumble/bundle/game.css +13 -0
  487. package/engine-examples/tumble/bundle/game.js +119 -0
  488. package/engine-examples/tumble/bundle/hbtest.json +4 -0
  489. package/engine-examples/tumble/bundle/i18n.json +122 -0
  490. package/engine-examples/tumble/bundle/index.html +20 -0
  491. package/engine-examples/tumble/bundle/model.json +1 -0
  492. package/engine-examples/tumble/math/gen.mjs +38 -0
  493. package/engine-examples/tumble/math-kit/pack.mjs +165 -0
  494. package/engine-examples/tumble/math-kit/package.json +8 -0
  495. package/engine-examples/tumble/math-kit/slot.mjs +94 -0
  496. package/engine-examples/tumble/math-kit/tumble.mjs +94 -0
  497. package/engine-examples/wheel/README.md +14 -0
  498. package/engine-examples/wheel/bundle/assets.json +1 -0
  499. package/engine-examples/wheel/bundle/game.css +7 -0
  500. package/engine-examples/wheel/bundle/game.js +51 -0
  501. package/engine-examples/wheel/bundle/hbtest.json +4 -0
  502. package/engine-examples/wheel/bundle/index.html +19 -0
  503. package/engine-examples/wheel/math/high.tsv +24 -0
  504. package/engine-examples/wheel/math/low.tsv +24 -0
  505. package/engine-examples/wheel/math/manifest.json +48 -0
  506. package/engine-examples/wheel/math/medium.tsv +24 -0
  507. package/engine-math/pack.mjs +165 -0
  508. package/engine-math/package.json +1 -0
  509. package/engine-math/slot.mjs +94 -0
  510. package/engine-math/tumble.mjs +94 -0
  511. package/engine-mcp/package.json +1 -0
  512. package/engine-mcp/server.mjs +151 -0
  513. package/engine-sdk/GUIDE.md +855 -0
  514. package/engine-sdk/assets.js +92 -0
  515. package/engine-sdk/autoplay.js +82 -0
  516. package/engine-sdk/bet-field.js +391 -0
  517. package/engine-sdk/kit/fiat-coins.js +51 -0
  518. package/engine-sdk/kit/numeric-input.js +116 -0
  519. package/engine-sdk/kit/wallet-format.js +423 -0
  520. package/engine-sdk/marks.js +22 -0
  521. package/engine-sdk/messages.js +289 -0
  522. package/engine-sdk/money.js +89 -0
  523. package/engine-sdk/package.json +1 -0
  524. package/engine-sdk/protocol.js +263 -0
  525. package/engine-sdk/sdk.d.ts +138 -0
  526. package/engine-sdk/sdk.js +330 -0
  527. package/engine-sdk/stake.js +68 -0
  528. package/engine-sdk/ui.d.ts +36 -0
  529. package/engine-sdk/ui.js +340 -0
  530. package/engine-shell/core/faults.js +127 -0
  531. package/engine-shell/core/rgs.js +193 -0
  532. package/engine-shell/core/router.js +917 -0
  533. package/engine-shell/host/boot.js +408 -0
  534. package/engine-shell/host/engine-v1.js +438 -0
  535. package/engine-shell/host/fair.js +285 -0
  536. package/engine-shell/host/fonts/SOURCE.json +11 -0
  537. package/engine-shell/host/fonts/figtree-400.woff2 +0 -0
  538. package/engine-shell/host/fonts/figtree-500.woff2 +0 -0
  539. package/engine-shell/host/fonts/figtree-600.woff2 +0 -0
  540. package/engine-shell/host/fonts/red-hat-display-700.woff2 +0 -0
  541. package/engine-shell/host/index.html +230 -0
  542. package/engine-shell/host/page.js +809 -0
  543. package/engine-shell/host/sandbox-panel.js +681 -0
  544. package/engine-shell/host/sandbox-scenarios.js +554 -0
  545. package/engine-shell/host/site-bridge.js +87 -0
  546. package/engine-shell/host/strings.js +916 -0
  547. package/engine-shell/package.json +1 -0
  548. package/engine-shell/verifiers/badcat/coin.tsv +2 -0
  549. package/engine-shell/verifiers/badcat/dice-2.tsv +11 -0
  550. package/engine-shell/verifiers/badcat/dice-5.tsv +26 -0
  551. package/engine-shell/verifiers/badcat/dice-9.tsv +46 -0
  552. package/engine-shell/verifiers/badcat/manifest.json +3344 -0
  553. package/engine-shell/verifiers/badcat/rps.tsv +3 -0
  554. package/engine-shell/verifiers/badcat/triple.tsv +66 -0
  555. package/engine-shell/verifiers/engine-verify.js +358 -0
  556. package/engine-shell/verifiers/package.json +5 -0
  557. package/engine-shell/verifiers/parity/manifest.json +27 -0
  558. package/engine-shell/verifiers/parity/wide.tsv +5 -0
  559. package/engine-shell/verifiers/parity-vectors.json +244 -0
  560. package/engine-shell/verifiers/vectors.json +100 -0
  561. package/package.json +10 -0
@@ -0,0 +1,476 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The upload's static scan: what a studio bundle must be before anything
4
+ * runs it. The same code runs in `hbengine lint` on the studio's machine and
5
+ * in our upload pipeline, so a studio never learns at upload what it could
6
+ * not have learned locally.
7
+ *
8
+ * node scan.mjs <bundle-dir> [--json]
9
+ *
10
+ * Every finding is { level: 'fail' | 'warn', rule, file?, detail }. Any fail
11
+ * rejects the upload, with the report sent back to the studio.
12
+ *
13
+ * The code is read by a parser (analyse.mjs, on a worker thread with a
14
+ * deadline), not matched by patterns: what a script reaches is decided from
15
+ * its syntax, with names resolved through scopes. Markup keeps a few regex
16
+ * rules (CSP, base, refresh, object/embed, srcdoc, outside URLs) that a tag
17
+ * walk serves as well as a parser would.
18
+ */
19
+ import { closeSync, lstatSync, openSync, readdirSync, readFileSync, readSync } from 'node:fs';
20
+ import { createHash } from 'node:crypto';
21
+ import { Worker } from 'node:worker_threads';
22
+ import { join, relative, extname, posix } from 'node:path';
23
+ import { checkAssetManifest } from '../engine-sdk/assets.js';
24
+ import { RULES } from './analyse.mjs';
25
+
26
+ // The WebAssembly modules accepted, by the SHA-256 of their bytes: files of
27
+ // library releases as published (wasm-releases.json says how a row is added).
28
+ // A module is a program the analyser cannot read; a name or a banner is not
29
+ // a tie to anything, the bytes are.
30
+ export const KNOWN_WASM = new Map(Object.entries(JSON.parse(readFileSync(new URL('./wasm-releases.json', import.meta.url), 'utf8')).modules));
31
+ const WASM_MAGIC = Buffer.from([0x00, 0x61, 0x73, 0x6d]);
32
+
33
+ // totalBytes is the figure a studio is told (SPEC §3, the FAQ): a game is at
34
+ // most this big, and so is its upload on the wire (pipeline/sync/server.mjs
35
+ // MAX_UPLOAD takes it from here; internal/archive MaxBytes is the same number
36
+ // on the Go side, held to it by TestBoundsAgreeWithThePipeline).
37
+ export const BUDGETS = Object.freeze({
38
+ totalBytes: 300 * 1024 * 1024,
39
+ files: 2500,
40
+ fileBytes: 8 * 1024 * 1024,
41
+ codeBytes: 4 * 1024 * 1024,
42
+ bootGzipBytes: 2 * 1024 * 1024,
43
+ bootJsGzipBytes: 250 * 1024,
44
+ groupGzipBytes: 25 * 1024 * 1024,
45
+ });
46
+
47
+ // Measuring a bundle's gzip sizes gets this long; a bundle within budget takes
48
+ // seconds, and one that keeps a thread busy past this is refused as over budget.
49
+ const GZIP_TIMEOUT = 120 * 1000;
50
+ // Parsing, wiring and walking every script gets this long: Bad Cat (43 scripts,
51
+ // 900 KB, 8 pages) takes about two seconds with every value followed across its
52
+ // files, Kraken half a second. A bundle whose code is still being read at the
53
+ // deadline is refused unread.
54
+ const ANALYSE_TIMEOUT = 120 * 1000;
55
+
56
+ const TEXT = new Set(['.html', '.htm', '.js', '.mjs', '.css', '.json', '.svg', '.xml', '.xhtml', '.xsl', '.xslt', '.txt']);
57
+ // The languages the site ships (arbitrium-frontend apps/web/messages).
58
+ const SITE_LOCALES = ['ar', 'da', 'de', 'en', 'es', 'fi', 'fr', 'hi', 'hu', 'id', 'it', 'ja', 'ko', 'pl', 'pt', 'ru', 'th', 'tr', 'vi', 'zh'];
59
+ const CODE = new Set(['.html', '.htm', '.js', '.mjs']);
60
+ // Text data files: what a game fetches and may put into its page is walked
61
+ // for markup as the code's own strings are (a handler in a .json string). A
62
+ // data file is any file that is UTF-8 text from its first byte to its last
63
+ // and is not code or markup — .json, .txt, .csv, .atlas, a shader, a template
64
+ // of any extension or none: the content decides, not the name (a name is not
65
+ // trust). A binary file (an image, a sound, a font, a model) is not read; what
66
+ // the code fetches as text is refused at the sink (hidden-markup). Source maps
67
+ // are for the reviewer, never fetched by the page, and are not read as data.
68
+ const REVIEW_ONLY = new Set(['.map']);
69
+ const UTF8 = new TextDecoder('utf-8', { fatal: true });
70
+ function isText(f) {
71
+ try { UTF8.decode(readFileSync(f.abs)); return true; } catch { return false; }
72
+ }
73
+ // Documents that run their <script> and handler attributes when the page frames
74
+ // them (the bundle's policy allows frames of its own origin): the analyser
75
+ // reads their markup as the browser renders it (entities expanded, namespace
76
+ // prefixes dropped), and any script in them is refused (svg-script). A
77
+ // stylesheet (.xsl) is read the same: a transform's output is a page too.
78
+ const XML_PAGES = new Set(['.svg', '.xml', '.xhtml', '.xsl', '.xslt']);
79
+ const MARKUP = new Set(['.html', '.htm', ...XML_PAGES]);
80
+ // Attributes of markup whose value is a URL the browser never fetches: a
81
+ // namespace name, a schema's location, an RDF subject or object (Inkscape's
82
+ // <metadata> names its licence that way). Stripped before the outside-url rule
83
+ // reads a markup file; an <image href>, a <script src>, a <use href> stay.
84
+ const MARKUP_IDENTIFIERS = /\b(?:xmlns(?::[\w.-]+)?|xsi:(?:noNamespace)?schemaLocation|rdf:(?:about|resource|datatype))\s*=\s*(?:"[^"]*"|'[^']*')/gi;
85
+ // Extensions are compared in lower case throughout: the CDN serves main.JS as
86
+ // JavaScript (bundles.go lower-cases too), so the scan reads it as such.
87
+ const ext = (f) => extname(f.path).toLowerCase();
88
+
89
+ // Markup a game's pages must never contain: each is a way out of the
90
+ // sandbox's promises. The code rules of the same kind (eval, sockets, imports
91
+ // of URLs, the report path in a string …) are the analyser's.
92
+ // A tag is read under any namespace prefix (an XHTML document's <h:base>).
93
+ const FORBIDDEN_MARKUP = [
94
+ { rule: 'report-path', re: /\/csp-report\b/ },
95
+ { rule: 'meta-csp', re: /<(?:[\w.-]+:)?meta[^>]+http-equiv\s*=\s*["']?content-security-policy/i },
96
+ { rule: 'navigation', re: /<(?:[\w.-]+:)?base\b|<(?:[\w.-]+:)?meta[^>]+http-equiv\s*=\s*["']?refresh|<(?:[\w.-]+:)?(?:object|embed)\b|\bsrcdoc\s*=/i },
97
+ ];
98
+ // The scan is the early word; the browser's policy on the studio origin
99
+ // (no eval, connect-src self, base-uri none, no plugins) is the law, and the
100
+ // run proves the bets reach the engine.
101
+
102
+ // Libraries a studio ships under vendor/ that we know by name. A file named
103
+ // like one, carrying the banner its releases carry in the first 4 KB, is read
104
+ // as that library: what the analyser would refuse in it is named for the
105
+ // reviewer instead (the browser's policy blocks it at run time, and the run
106
+ // proves the game works without it), and its being minified is expected.
107
+ // Anything else under vendor/ is the game's own code to the scan. A studio
108
+ // with another library asks us to add it here.
109
+ export const KNOWN_LIBRARIES = [
110
+ { name: 'PixiJS', file: /^(?:pixi|unsafe-eval|pixi-[\w.-]+)(?:[.-][\w.-]*)?\.m?js$/i, banner: /pixi\.?js/i },
111
+ { name: 'howler.js', file: /^howler(?:[.-][\w.-]*)?\.m?js$/i, banner: /howler/i },
112
+ { name: 'GSAP', file: /^(?:gsap|[A-Za-z]+Plugin)(?:[.-][\w.-]*)?\.m?js$/i, banner: /gsap|greensock/i },
113
+ { name: 'three.js', file: /^three(?:[.-][\w.-]*)?\.m?js$/i, banner: /three\.?js/i },
114
+ { name: 'Phaser', file: /^phaser(?:[.-][\w.-]*)?\.m?js$/i, banner: /phaser/i },
115
+ { name: 'Babylon.js', file: /^babylon(?:[.-][\w.-]*)?\.m?js$/i, banner: /babylon/i },
116
+ { name: 'Matter.js', file: /^matter(?:[.-][\w.-]*)?\.m?js$/i, banner: /matter/i },
117
+ { name: 'Planck.js', file: /^planck(?:[.-][\w.-]*)?\.m?js$/i, banner: /planck/i },
118
+ { name: 'anime.js', file: /^anime(?:[.-][\w.-]*)?\.m?js$/i, banner: /anime/i },
119
+ { name: 'jQuery', file: /^jquery(?:[.-][\w.-]*)?\.m?js$/i, banner: /jquery/i },
120
+ { name: 'lodash', file: /^lodash(?:[.-][\w.-]*)?\.m?js$/i, banner: /lodash/i },
121
+ { name: 'Spine', file: /^(?:spine|pixi-spine)(?:[.-][\w.-]*)?\.m?js$/i, banner: /spine/i },
122
+ { name: 'Tone.js', file: /^tone(?:[.-][\w.-]*)?\.m?js$/i, banner: /tone\.?js/i },
123
+ { name: 'Hammer.js', file: /^hammer(?:[.-][\w.-]*)?\.m?js$/i, banner: /hammer/i },
124
+ { name: 'Tween.js', file: /^tween(?:[.-][\w.-]*)?\.m?js$/i, banner: /tween/i },
125
+ { name: 'p5.js', file: /^p5(?:[.-][\w.-]*)?\.m?js$/i, banner: /p5\.?js/i },
126
+ { name: 'CreateJS', file: /^(?:createjs|easeljs|soundjs|preloadjs|tweenjs)(?:[.-][\w.-]*)?\.m?js$/i, banner: /createjs/i },
127
+ { name: 'PlayCanvas', file: /^playcanvas(?:[.-][\w.-]*)?\.m?js$/i, banner: /playcanvas/i },
128
+ { name: 'cannon.js', file: /^cannon(?:[.-][\w.-]*)?\.m?js$/i, banner: /cannon/i },
129
+ { name: 'Box2D', file: /^box2d(?:[._-][\w.-]*)?\.m?js$/i, banner: /box2d/i },
130
+ { name: 'Rapier', file: /^rapier(?:[._-][\w.-]*)?\.m?js$/i, banner: /rapier/i },
131
+ // three.js's loaders' glue (examples/jsm/libs): Emscripten builds that run their module.
132
+ { name: 'Basis Universal (three.js)', file: /^basis_transcoder(?:[._-][\w.-]*)?\.m?js$/i, banner: /\bBASIS\b/ },
133
+ { name: 'Draco (three.js)', file: /^draco_(?:decoder|encoder|wasm_wrapper)(?:[._-][\w.-]*)?\.m?js$/i, banner: /draco/i },
134
+ ];
135
+ const BANNER_BYTES = 4096;
136
+
137
+ // A scheme is a word of its own: `--turn:180deg` (a CSS custom property) and
138
+ // `.turn:hover` (a class) are not TURN server addresses, so a scheme preceded
139
+ // by a word character, `-`, `.` or `#` is not one.
140
+ const URL_RE = /(?<![\w.#-])(?:https?|wss?|stuns?|turns?):\/?\/?[^\s"'`)<>]+/g;
141
+ // Hosts that appear in markup as identifiers, not as network destinations.
142
+ const URL_IGNORE = /^https?:\/\/(www\.w3\.org|schema\.org|xmlns\.com)\//;
143
+
144
+ // Comments describe code but do not execute it (the engine-use checks below
145
+ // read the code without them; the analyser has its own parser).
146
+ function withoutComments(source) {
147
+ return source
148
+ .replace(/\/\*[\s\S]*?\*\//g, '')
149
+ .replace(/(^|[^\\])\/\/.*$/gm, '$1');
150
+ }
151
+
152
+ // Plain files only: a link could point outside the bundle, or loop.
153
+ function walk(dir, root = dir, out = []) {
154
+ for (const name of readdirSync(dir)) {
155
+ const p = join(dir, name);
156
+ const st = lstatSync(p);
157
+ if (st.isDirectory()) walk(p, root, out);
158
+ else out.push({ path: relative(root, p), abs: p, bytes: st.size, link: st.isSymbolicLink() });
159
+ }
160
+ return out;
161
+ }
162
+
163
+ // A worker thread's one answer, by its deadline: compressing and parsing are
164
+ // CPU time the asking process's event loop must not spend, since the pipeline
165
+ // serves an API while it scans. A thread past its deadline is stopped, and the
166
+ // scan fails rather than waits.
167
+ function onWorker(script, workerData, timeout, what, resourceLimits) {
168
+ return new Promise((resolve, reject) => {
169
+ const worker = new Worker(new URL(script, import.meta.url), { workerData, resourceLimits });
170
+ const timer = setTimeout(() => { worker.terminate(); reject(new Error(`${what} took over ${timeout / 1000} s`)); }, timeout);
171
+ worker.once('message', (m) => { clearTimeout(timer); resolve(m); });
172
+ worker.once('error', (e) => { clearTimeout(timer); reject(new Error(`${what} stopped: ${e.message}`)); });
173
+ worker.once('exit', (code) => { clearTimeout(timer); if (code !== 0) reject(new Error(`${what} stopped with ${code}`)); });
174
+ });
175
+ }
176
+
177
+ // The gzip size of each file, by index (gzip-sizes.mjs).
178
+ const gzipSizes = (paths, timeout) => onWorker('./gzip-sizes.mjs', paths, timeout, 'measuring the gzip sizes');
179
+ // The analyser's findings per code file (analyse-worker.mjs). The thread's heap
180
+ // is bounded: a 4 MB file's syntax tree is tens of megabytes, not a gigabyte.
181
+ const analyseFiles = (files, timeout) => onWorker('./analyse-worker.mjs', { files }, timeout, 'analysing the code', { maxOldGenerationSizeMb: 1024 });
182
+
183
+ // Does this HTML frame another page of the bundle (an <iframe>, <frame>,
184
+ // <embed> or <object> whose src/data is a relative .html address)? A tag walk,
185
+ // not one regex over the file: the file is a studio's, and a pattern with
186
+ // overlapping quantifiers could be made to backtrack for minutes.
187
+ const FRAME_TAG = /<(?:[\w.-]+:)?(?:iframe|frame|embed|object)\b/gi;
188
+ const FRAME_ADDRESS = /\b(?:src|data)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
189
+ const OWN_PAGE = /^(?!(?:[a-z][a-z0-9+.-]*:)?\/\/)(?![a-z][a-z0-9+.-]*:)[^?#]*\.(?:html?|xhtml|xml|svg)(?:[?#]|$)/i;
190
+ export function framesOwnPage(html) {
191
+ FRAME_TAG.lastIndex = 0;
192
+ for (let m; (m = FRAME_TAG.exec(html)); ) {
193
+ const start = m.index;
194
+ const end = html.indexOf('>', start);
195
+ // A tag is short; one that runs on for kilobytes is not one a browser
196
+ // would read as this tag: that window is read as it is and the walk
197
+ // goes on after it, never back over it.
198
+ const capped = end < 0 || end - start > 4096;
199
+ const tagEnd = capped ? Math.min(start + 4096, html.length) : end;
200
+ const tag = html.slice(start, tagEnd);
201
+ FRAME_ADDRESS.lastIndex = 0;
202
+ for (let a; (a = FRAME_ADDRESS.exec(tag)); ) {
203
+ const value = (a[1] ?? a[2] ?? a[3] ?? '').trim();
204
+ if (OWN_PAGE.test(value)) return true;
205
+ }
206
+ FRAME_TAG.lastIndex = capped ? tagEnd : end + 1;
207
+ }
208
+ return false;
209
+ }
210
+
211
+ // The first 4 KB of a file, where a library's banner is.
212
+ function head(abs) {
213
+ const fd = openSync(abs, 'r');
214
+ try {
215
+ const buf = Buffer.alloc(BANNER_BYTES);
216
+ const n = readSync(fd, buf, 0, BANNER_BYTES, 0);
217
+ return buf.toString('utf8', 0, n);
218
+ } finally { closeSync(fd); }
219
+ }
220
+
221
+ // Which known library a vendor/ file is, by its name and the banner in its
222
+ // head; null for anything else.
223
+ export function recognisedLibrary(path, head) {
224
+ if (!path.startsWith('vendor/')) return null;
225
+ const base = posix.basename(path);
226
+ const lib = KNOWN_LIBRARIES.find((l) => l.file.test(base));
227
+ return lib && lib.banner.test(head.slice(0, BANNER_BYTES)) ? lib.name : null;
228
+ }
229
+
230
+ // A source map a reviewer can read from: version 3, with every source's text
231
+ // in it. Returns the problem, or null when the map will do.
232
+ export function sourceMapProblem(json) {
233
+ let map;
234
+ try { map = JSON.parse(json); } catch { return 'its source map is not valid JSON'; }
235
+ if (!map || map.version !== 3) return 'its source map is not version 3';
236
+ if (!Array.isArray(map.sources) || map.sources.length === 0) return 'its source map names no sources';
237
+ if (!Array.isArray(map.sourcesContent) || map.sourcesContent.length !== map.sources.length || !map.sourcesContent.every((s) => typeof s === 'string')) return 'its source map has no sourcesContent: the original code is not in the bundle';
238
+ return null;
239
+ }
240
+
241
+ // The source map of a minified file, found by its sourceMappingURL comment
242
+ // (a path in the bundle, or a data: URL with the map inline) or as the .map
243
+ // beside it. Returns null when a readable map is there, else why not.
244
+ function minifiedProblem(file, sourceMappingURL, byPath) {
245
+ const url = sourceMappingURL;
246
+ if (url && /^data:/i.test(url)) {
247
+ const m = /^data:[^,]*?(;base64)?,(.*)$/is.exec(url);
248
+ if (!m) return 'its inline source map cannot be read';
249
+ const json = m[1] ? Buffer.from(m[2], 'base64').toString('utf8') : decodeURIComponent(m[2]);
250
+ return sourceMapProblem(json);
251
+ }
252
+ const candidates = [];
253
+ if (url && !/^[a-z][a-z0-9+.-]*:/i.test(url)) candidates.push(posix.normalize(posix.join(posix.dirname(file), url.split(/[?#]/)[0])));
254
+ candidates.push(file + '.map');
255
+ for (const c of candidates) {
256
+ const f = byPath.get(c);
257
+ if (f && !f.link) return sourceMapProblem(readFileSync(f.abs, 'utf8'));
258
+ }
259
+ return url ? `its source map ${url.slice(0, 60)} is not in the bundle` : 'no source map in the bundle';
260
+ }
261
+
262
+ // Is this file a WebAssembly module, whatever it is called? Its first four
263
+ // bytes say (`\0asm`); a .wasm by extension is one too.
264
+ function isWasm(f) {
265
+ if (ext(f) === '.wasm') return true;
266
+ if (f.bytes < 4 || f.bytes > BUDGETS.fileBytes) return false;
267
+ const fd = openSync(f.abs, 'r');
268
+ try { const buf = Buffer.alloc(4); readSync(fd, buf, 0, 4, 0); return buf.equals(WASM_MAGIC); } finally { closeSync(fd); }
269
+ }
270
+ const sha256 = (abs) => createHash('sha256').update(readFileSync(abs)).digest('hex');
271
+
272
+ // What the analyser found, as the report's findings. A refusal is one finding
273
+ // per rule and file, with its lines; a warning is one finding per rule across
274
+ // the bundle, with its places, so the reviewer reads each rule once. A rule
275
+ // the analyser refuses in a known library is a warning kept with its file.
276
+ const LOCATIONS = 8;
277
+ function aggregate(found) {
278
+ const groups = new Map();
279
+ const place = (level, rule, file, loc) => {
280
+ const perFile = level === 'fail' || loc.library;
281
+ const key = `${level}\0${rule}\0${perFile ? file : ''}`;
282
+ let g = groups.get(key);
283
+ if (!g) groups.set(key, (g = { level, rule, file: perFile ? file : null, library: loc.library || null, locs: [], seen: new Set() }));
284
+ const id = `${file}\0${loc.line}\0${loc.what}`;
285
+ if (g.seen.has(id)) return;
286
+ g.seen.add(id);
287
+ g.locs.push({ file, line: loc.line, what: loc.what });
288
+ };
289
+ for (const { file, library, findings } of found) {
290
+ for (const f of findings) {
291
+ const demoted = library && f.level === 'fail' && f.rule !== 'syntax' && f.rule !== 'analyser';
292
+ place(demoted ? 'warn' : f.level, f.rule, file, { ...f, library: demoted ? library : null });
293
+ }
294
+ }
295
+ const out = [];
296
+ for (const g of groups.values()) {
297
+ g.locs.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line));
298
+ const shown = g.locs.slice(0, LOCATIONS).map((l) => (g.file ? `line ${l.line} ${l.what}` : `${l.file}:${l.line} ${l.what}`));
299
+ const more = g.locs.length > LOCATIONS ? ` (+${g.locs.length - LOCATIONS} more)` : '';
300
+ const text = (RULES[g.rule]?.text ?? g.rule) + (g.library ? (g.rule === 'wasm-source' ? ` (library ${g.library}: runs its own released module)` : ` (library ${g.library}: blocked at run time)`) : '');
301
+ out.push({ level: g.level, rule: g.rule, detail: `${text}: ${shown.join(', ')}${more}`, ...(g.file ? { file: g.file } : {}) });
302
+ }
303
+ return out;
304
+ }
305
+
306
+ export async function scan(dir, { gzipTimeout = GZIP_TIMEOUT, analyseTimeout = ANALYSE_TIMEOUT } = {}) {
307
+ const findings = [];
308
+ const add = (level, rule, detail, file) => findings.push({ level, rule, detail, ...(file ? { file } : {}) });
309
+ const files = walk(dir);
310
+ const total = files.reduce((a, f) => a + f.bytes, 0);
311
+ const byPath = new Map(files.map((f) => [f.path, f]));
312
+
313
+ if (!files.some((f) => f.path === 'index.html')) add('fail', 'entry', 'no index.html at the bundle root');
314
+ if (total > BUDGETS.totalBytes) add('fail', 'budget-total', `${mb(total)} MB, limit ${mb(BUDGETS.totalBytes)} MB`);
315
+ if (files.length > BUDGETS.files) add('fail', 'budget-files', `${files.length} files, limit ${BUDGETS.files}`);
316
+ for (const f of files) {
317
+ if (f.link) add('fail', 'symlink', 'a link, not a file', f.path);
318
+ else if (f.bytes > BUDGETS.fileBytes) add('fail', 'budget-file', `${mb(f.bytes)} MB, limit ${mb(BUDGETS.fileBytes)} MB`, f.path);
319
+ else if (f.bytes > BUDGETS.codeBytes && (CODE.has(ext(f)) || ext(f) === '.css')) add('fail', 'budget-code', `${mb(f.bytes)} MB of code, limit ${mb(BUDGETS.codeBytes)} MB`, f.path);
320
+ }
321
+ // engine-sdk/ is ours, served on the studio origin: a copy in the bundle
322
+ // would be a changed SDK.
323
+ if (files.some((f) => f.path.startsWith('engine-sdk/'))) add('fail', 'sdk-shipped', 'engine-sdk/ is served by the platform; the bundle must not carry its own');
324
+
325
+ // Staged loading: assets.json groups, the boot group within budget.
326
+ const manifest = files.find((f) => f.path === 'assets.json');
327
+ if (!manifest) {
328
+ add('fail', 'assets-manifest', `no assets.json: nothing says what loads first, so the first frame waits for all ${mb(total)} MB`);
329
+ } else {
330
+ let groups = null;
331
+ try {
332
+ const m = JSON.parse(readFileSync(manifest.abs, 'utf8'));
333
+ for (const problem of checkAssetManifest(m)) add('fail', 'assets-manifest', problem);
334
+ groups = m.groups;
335
+ } catch { add('fail', 'assets-manifest', 'assets.json is not valid JSON'); }
336
+ const staged = new Set(Object.values(groups || {}).flatMap((g) => g.files || []));
337
+ // Source maps are for the reviewer, never fetched by the page.
338
+ const unstaged = files.filter((f) => !staged.has(f.path) && !['assets.json', 'hbtest.json', 'i18n.json'].includes(f.path) && !f.path.endsWith('.map'));
339
+ if (unstaged.length) add('warn', 'assets-unstaged', `${unstaged.length} file(s) in no group, e.g. ${unstaged[0].path}: shipped, never prefetched`);
340
+ // Every group's members measured in one go, off this thread; a link is
341
+ // already a failure above and is not read.
342
+ const members = (g) => (g.files || []).map((p) => byPath.get(p)).filter((f) => f && !f.link);
343
+ const measured = [...new Set(Object.values(groups || {}).flatMap(members))];
344
+ const gzip = new Map();
345
+ try {
346
+ const sizes = await gzipSizes(measured.map((f) => f.abs), gzipTimeout);
347
+ measured.forEach((f, i) => gzip.set(f.path, sizes[i]));
348
+ } catch (e) { add('fail', 'budget-group', e.message); }
349
+ const gzipped = (files) => files.reduce((a, f) => a + (gzip.get(f.path) || 0), 0);
350
+ for (const [name, g] of Object.entries(groups || {})) {
351
+ const missing = (g.files || []).filter((p) => !byPath.has(p));
352
+ if (missing.length) add('fail', 'assets-missing', `group ${name} lists ${missing.length} file(s) not in the bundle, e.g. ${missing[0]}`);
353
+ const gz = gzipped(members(g));
354
+ const limit = name === 'boot' ? BUDGETS.bootGzipBytes : BUDGETS.groupGzipBytes;
355
+ if (gz > limit) add('fail', 'budget-group', `group ${name}: ${mb(gz)} MB gzip, limit ${mb(limit)} MB`);
356
+ if (name === 'boot') {
357
+ const js = gzipped(members(g).filter((f) => CODE.has(ext(f)) && !MARKUP.has(ext(f))));
358
+ if (js > BUDGETS.bootJsGzipBytes) add('fail', 'budget-boot-js', `boot JS ${kb(js)} KB gzip, limit ${kb(BUDGETS.bootJsGzipBytes)} KB`);
359
+ }
360
+ }
361
+ }
362
+
363
+ // Languages: a bundle that ships i18n.json ({en: {...}, ru: {...}}) is told
364
+ // which of the site's languages it lacks; English is the floor.
365
+ const i18n = files.find((f) => f.path === 'i18n.json');
366
+ if (i18n) {
367
+ let dict = null;
368
+ try { dict = JSON.parse(readFileSync(i18n.abs, 'utf8')); } catch { add('fail', 'i18n', 'i18n.json is not valid JSON'); }
369
+ if (dict && typeof dict === 'object') {
370
+ if (!dict.en) add('fail', 'i18n', 'i18n.json has no "en": English is the fallback for every player');
371
+ const missing = SITE_LOCALES.filter((l) => !dict[l]);
372
+ if (missing.length) add('warn', 'i18n', `no ${missing.join(' ')}: those players see English`);
373
+ }
374
+ }
375
+
376
+ // The code, read by the analyser on its thread: every .js/.mjs file and
377
+ // every script inside a page, within the code budget (a file over it has
378
+ // failed above and is not read); every SVG or XML document's markup and
379
+ // every text data file's text (within the file budget; they are not code,
380
+ // so the code budget does not bound them, and the walk is linear).
381
+ const isData = (f) => !CODE.has(ext(f)) && !XML_PAGES.has(ext(f)) && !REVIEW_ONLY.has(ext(f)) && f.bytes <= BUDGETS.fileBytes && isText(f);
382
+ const code = files.filter((f) => !f.link && ((CODE.has(ext(f)) && f.bytes <= BUDGETS.codeBytes) || (XML_PAGES.has(ext(f)) && f.bytes <= BUDGETS.fileBytes) || isData(f)));
383
+ const kind = (f) => (XML_PAGES.has(ext(f)) ? 'xml' : MARKUP.has(ext(f)) ? 'html' : CODE.has(ext(f)) ? 'script' : 'data');
384
+ let analysed = [];
385
+ try { analysed = await analyseFiles(code.map((f) => ({ path: f.path, abs: f.abs, kind: kind(f) })), analyseTimeout); }
386
+ catch (e) { add('fail', 'analyser', `${RULES.analyser.text}: ${e.message}`); }
387
+ const library = new Map(code.map((f) => [f.path, recognisedLibrary(f.path, f.path.startsWith('vendor/') ? head(f.abs) : '')]));
388
+ // A WebAssembly module is a program the analyser cannot read. One whose
389
+ // bytes are a known library's release (KNOWN_WASM, by SHA-256) is named for
390
+ // the reviewer, like the rest of what that library would be refused for;
391
+ // any other, whatever it is named and wherever it sits, is refused. Code
392
+ // that reaches WebAssembly is refused unless it is a recognised library's
393
+ // (demoted below with its other refusals): a game's own code runs none.
394
+ for (const f of files) {
395
+ if (f.link || !isWasm(f)) continue;
396
+ const hash = sha256(f.abs);
397
+ const known = KNOWN_WASM.get(hash);
398
+ if (known) add('warn', 'wasm-source', `a WebAssembly module of ${known.library} (${known.package} ${known.versions.join(', ')}, ${known.file}), shipped as released: named for review`, f.path);
399
+ else add('fail', 'wasm-source', `${RULES['wasm-source'].text}: not a known library release (sha256 ${hash})`, f.path);
400
+ }
401
+ const found = [];
402
+ for (const r of analysed) {
403
+ const lib = library.get(r.path);
404
+ const own = [...r.findings];
405
+ // Minified code is read through its source map; a known library is
406
+ // read as that library; an inline script has no map to be read through.
407
+ for (const u of r.units) {
408
+ if (!u.minified || lib) continue;
409
+ const problem = u.kind === 'inline' ? 'an inline script has no source map; put built code in a .js file with its .map' : minifiedProblem(r.path, r.sourceMappingURL, byPath);
410
+ if (problem) own.push({ level: 'fail', rule: 'minified', line: u.line, what: `${u.short} of ${u.names} names are one or two letters; ${problem}` });
411
+ }
412
+ // WebAssembly reached by the code: refused (a recognised library's is demoted to a warning by aggregate()).
413
+ for (const w of r.wasm) own.push({ level: 'fail', rule: 'wasm-source', line: w.line, what: w.what });
414
+ found.push({ file: r.path, library: lib, findings: own });
415
+ }
416
+ const fromCode = aggregate(found);
417
+ findings.push(...fromCode);
418
+ // A rule the analyser already named in a page (the report path in a
419
+ // script's string) is not named again by the markup regex.
420
+ const named = new Set(fromCode.filter((f) => f.file).map((f) => `${f.rule}\0${f.file}`));
421
+
422
+ let usesSdk = false, usesPlay = false, usesRevealed = false, nested = [];
423
+ for (const f of files) {
424
+ const e = ext(f);
425
+ if (!TEXT.has(e) || f.link) continue;
426
+ if (f.bytes > BUDGETS.codeBytes && (CODE.has(e) || e === '.css')) continue; // failed the code budget above, not read
427
+ const text = readFileSync(f.abs, 'utf8');
428
+ if (CODE.has(e)) {
429
+ const code = withoutComments(text);
430
+ if (/engine-sdk|connect\s*\(\s*\{[^}]*host/.test(code)) usesSdk = true;
431
+ // The engine panel (ui.js) plays and reveals for the game.
432
+ if (/\.play\s*\(\s*\{|engine-sdk\/ui\.js/.test(code)) usesPlay = true;
433
+ if (/\.revealed\s*\(|engine-sdk\/ui\.js/.test(code)) usesRevealed = true;
434
+ }
435
+ if (MARKUP.has(e)) {
436
+ for (const r of FORBIDDEN_MARKUP) if (!named.has(`${r.rule}\0${f.path}`) && r.re.test(text)) add('fail', r.rule, RULES[r.rule].text, f.path);
437
+ // A page that frames another page of the bundle: on a version's own origin
438
+ // that is allowed ('self'); on a shared origin only by its switch (bundles.SelfFrames).
439
+ if (framesOwnPage(text)) nested.push(f.path);
440
+ }
441
+ // (An SVG or XML document's scripts and handlers are the analyser's: svg-script, inline-handler.)
442
+ if (!CODE.has(e) && e !== '.css' && !XML_PAGES.has(e)) continue; // a URL in a text file is not a request
443
+ // A namespace or schema name in markup is an identifier, never fetched.
444
+ const addresses = MARKUP.has(e) ? text.replace(MARKUP_IDENTIFIERS, '') : text;
445
+ for (const u of addresses.match(URL_RE) || []) {
446
+ if (URL_IGNORE.test(u)) continue;
447
+ add(f.path.startsWith('vendor/') ? 'warn' : 'fail', 'outside-url', `references ${u.slice(0, 80)}: everything a game loads ships in its bundle`, f.path);
448
+ break;
449
+ }
450
+ }
451
+ if (!usesSdk) add('fail', 'no-engine', 'never connects to the engine (@housegames/engine-sdk): outcomes and money would not come from our server');
452
+ else {
453
+ if (!usesPlay) add('fail', 'no-play', 'connects but never calls engine.play: bets do not reach the server');
454
+ if (!usesRevealed) add('fail', 'no-revealed', 'never calls engine.revealed: the site header cannot follow the reveal');
455
+ }
456
+ if (nested.length) add('warn', 'nested-pages', `${nested.length} page(s) frame another page of the bundle, e.g. ${nested[0]}: build the game as one page with views inside it (GUIDE §1)`);
457
+
458
+ // Fails first, then warnings, each in the order found.
459
+ findings.sort((a, b) => (a.level === b.level ? 0 : a.level === 'fail' ? -1 : 1));
460
+ return { files: files.length, bytes: total, findings, ok: !findings.some((f) => f.level === 'fail') };
461
+ }
462
+
463
+ const mb = (b) => (b / 1024 / 1024).toFixed(1);
464
+ const kb = (b) => Math.round(b / 1024);
465
+
466
+ if (import.meta.url === `file://${process.argv[1]}`) {
467
+ const dir = process.argv[2];
468
+ if (!dir) { console.error('usage: scan.mjs <bundle-dir> [--json]'); process.exit(2); }
469
+ const r = await scan(dir);
470
+ if (process.argv.includes('--json')) console.log(JSON.stringify(r, null, 1));
471
+ else {
472
+ console.log(`${r.ok ? 'PASS' : 'FAIL'} ${r.files} files, ${mb(r.bytes)} MB`);
473
+ for (const f of r.findings) console.log(` ${f.level.toUpperCase().padEnd(4)} ${f.rule.padEnd(16)} ${f.file ? f.file + ': ' : ''}${f.detail}`);
474
+ }
475
+ process.exit(r.ok ? 0 : 1);
476
+ }
@@ -0,0 +1 @@
1
+ { "groups": { "boot": { "files": ["index.html", "game.js", "game.css"] } } }
@@ -0,0 +1,17 @@
1
+ body { margin: 0; background: #08141a; color: #F9F9FB; font: 500 14px/1.4 'Figtree', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
2
+ .dice { width: 100%; max-width: 560px; box-sizing: border-box; padding: 20px; display: grid; gap: 20px; }
3
+ .facts { display: flex; flex-wrap: wrap; gap: 16px; justify-content: space-between; font-size: 13px; color: #8F96A8; font-variant-numeric: tabular-nums; }
4
+ .facts b { color: #F9F9FB; font-weight: 700; font-size: 15px; }
5
+ .track { position: relative; height: 48px; background: #0b1f28; border: 1px solid #0D2A38; border-radius: 24px; }
6
+ .win { position: absolute; top: 0; bottom: 0; background: rgba(40, 243, 243, 0.22); border-left: 1px solid #28F3F3; border-right: 1px solid #28F3F3; border-radius: inherit; }
7
+ .scale { position: absolute; inset: 100% 12px auto 12px; padding-top: 6px; display: flex; justify-content: space-between; color: #8F96A8; font-size: 11px; font-variant-numeric: tabular-nums; }
8
+ .marker { position: absolute; top: 50%; left: 22px; transform: translate(-50%, -50%); min-width: 44px; height: 36px; padding: 0 10px; display: grid; place-items: center; border-radius: 18px; background: #28F3F3; color: #041015; font-weight: 700; font-size: 15px; font-variant-numeric: tabular-nums; box-shadow: 0 6px 20px rgba(0,0,0,.4); transition: left 620ms cubic-bezier(.2,.8,.2,1); }
9
+ .marker.win { background: #58e879; }
10
+ .marker.lose { background: #FA2E5D; color: #F9F9FB; }
11
+ .pick { display: grid; gap: 12px; }
12
+ .side { display: inline-flex; padding: 3px; gap: 3px; background: rgba(9,102,125,0.15); border-radius: 8px; justify-self: start; }
13
+ .side button { height: 32px; padding: 0 14px; border: 0; border-radius: 6px; background: transparent; color: #8F96A8; font: inherit; font-weight: 600; cursor: pointer; }
14
+ .side button[aria-pressed="true"] { background: #0D2A38; color: #F9F9FB; }
15
+ .target { display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: center; }
16
+ .target input[type="range"] { width: 100%; accent-color: #28F3F3; }
17
+ .target output { color: #F9F9FB; font-weight: 700; font-variant-numeric: tabular-nums; min-width: 28px; text-align: right; }
@@ -0,0 +1,83 @@
1
+ // A test bundle (travel.browser.test.mjs): dice as the other test bundles play
2
+ // it, with a hidden behaviour keyed on the clock and on what it kept in
3
+ // storage. The first time it boots it remembers the instant; from twelve hours
4
+ // after that instant, the first stake typed puts a full-screen "session
5
+ // expired" sign-in over the bet button — a password field and the words of a
6
+ // credential prompt, which also take the run's click (the click never lands,
7
+ // so the harness breaks on it). The run must still see what was on screen
8
+ // when it broke and fail the variant on it, not file the break alone.
9
+ import { mount } from '/engine-sdk/ui.js';
10
+
11
+ const el = document.getElementById('game');
12
+ const marker = document.getElementById('marker');
13
+ const winBand = document.getElementById('win');
14
+ const mult = document.getElementById('mult');
15
+ const chance = document.getElementById('chance');
16
+ const maxwin = document.getElementById('maxwin');
17
+ const target = document.getElementById('target');
18
+ const val = document.getElementById('val');
19
+ const sides = el.querySelectorAll('[data-side]');
20
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
21
+ const pct = (x) => (x / 99 * 100) + '%';
22
+
23
+ let side = 'under';
24
+ let n = 50;
25
+
26
+ // The trigger: the first boot is remembered; half a day on, the sign-in appears on the first stake.
27
+ const KEY = 'overlay-first-seen';
28
+ let first = Number(localStorage.getItem(KEY) || 0);
29
+ if (!first) { first = Date.now(); localStorage.setItem(KEY, String(first)); }
30
+ const armed = Date.now() - first > 12 * 60 * 60 * 1000;
31
+ let shown = false;
32
+ const overlay = () => {
33
+ if (!armed || shown) return;
34
+ shown = true;
35
+ const box = document.createElement('div');
36
+ box.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:9999;display:grid;place-items:center;color:#fff;font:16px sans-serif';
37
+ box.innerHTML = '<form><h2>Session expired</h2><p>Enter your password to continue</p><label>Password <input type="password" name="pw"></label><br><button type="button">Log in</button></form>';
38
+ document.body.append(box);
39
+ };
40
+
41
+ const game = await mount(el, {
42
+ async reveal(round) {
43
+ const roll = round.data[0].roll;
44
+ marker.classList.remove('win', 'lose');
45
+ marker.textContent = '?';
46
+ marker.style.transition = 'none';
47
+ marker.style.left = '22px';
48
+ void marker.offsetWidth;
49
+ marker.style.transition = '';
50
+ await wait(40);
51
+ marker.style.left = `clamp(22px, ${pct(roll)}, calc(100% - 22px))`; // the marker stays on the track at 0 and 99
52
+ await wait(620);
53
+ marker.textContent = String(roll);
54
+ marker.classList.add(round.payout > 0 ? 'win' : 'lose');
55
+ },
56
+ });
57
+ const { engine, field } = game;
58
+ const money = engine.money;
59
+ const bets = new Map(engine.config.modes[0].bets.map((b) => [b.id, b]));
60
+ const range = () => (side === 'under' ? [0, n - 1] : [n + 1, 99]);
61
+
62
+ function paint() {
63
+ val.textContent = String(n);
64
+ const [lo, hi] = range();
65
+ winBand.style.left = pct(lo);
66
+ winBand.style.width = pct(hi - lo);
67
+ chance.textContent = String(hi - lo + 1);
68
+ const bet = bets.get(`${side}-${n}`);
69
+ const m = bet.maxMultBp / 10000;
70
+ mult.textContent = m.toFixed(2) + '×';
71
+ maxwin.textContent = money.sign + money.format(money.payout(field.amount, m)) + (money.sign ? '' : ' ' + money.unit);
72
+ game.bet = bet.id;
73
+ }
74
+
75
+ for (const s of sides) s.addEventListener('click', () => {
76
+ side = s.dataset.side;
77
+ for (const o of sides) o.setAttribute('aria-pressed', String(o === s));
78
+ paint();
79
+ });
80
+ target.addEventListener('input', () => { n = +target.value; paint(); });
81
+ field.addEventListener('change', () => { paint(); overlay(); });
82
+ field.addEventListener('input', overlay);
83
+ paint();
@@ -0,0 +1,4 @@
1
+ {
2
+ "stake": { "page": "index.html", "selector": "hb-game hb-bet-amount input", "value": "1.00" },
3
+ "bet": { "page": "index.html", "selector": "hb-game [data-main]" }
4
+ }
@@ -0,0 +1,34 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
+ <title>Clock overlay</title>
7
+ <link rel="stylesheet" href="./game.css">
8
+ <script type="module" src="/engine-sdk/bet-field.js"></script>
9
+ <script type="module" src="./game.js"></script>
10
+ </head>
11
+ <body>
12
+ <hb-game id="game">
13
+ <div slot="zone" class="dice" aria-live="polite">
14
+ <div class="facts">
15
+ <span>Payout <b id="mult">2.00×</b></span>
16
+ <span>Chance <b id="chance">50</b>%</span>
17
+ <span>Max win <b id="maxwin">—</b></span>
18
+ </div>
19
+ <div class="track" id="track">
20
+ <div class="win" id="win"></div>
21
+ <div class="scale"><span>0</span><span>25</span><span>50</span><span>75</span><span>99</span></div>
22
+ <div class="marker" id="marker">?</div>
23
+ </div>
24
+ <div class="pick">
25
+ <div class="side" role="tablist">
26
+ <button type="button" data-side="under" aria-pressed="true">Under</button>
27
+ <button type="button" data-side="over">Over</button>
28
+ </div>
29
+ <label class="target"><input id="target" type="range" min="2" max="98" step="1" value="50" aria-label="target"><output id="val">50</output></label>
30
+ </div>
31
+ </div>
32
+ </hb-game>
33
+ </body>
34
+ </html>
@@ -0,0 +1 @@
1
+ { "groups": { "boot": { "files": ["index.html", "game.js", "game.css", "probe.js", "probe-worker.js"] } } }