@overcrow/mcp 0.0.0 → 0.1.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 (489) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +91 -1
  3. package/dist/audit/index.js +103 -0
  4. package/dist/audit/lexer.js +417 -0
  5. package/dist/audit/project.js +200 -0
  6. package/dist/audit/rules.js +661 -0
  7. package/dist/bootstrap/download.js +229 -0
  8. package/dist/bootstrap/install.js +280 -0
  9. package/dist/bootstrap/pin.js +85 -0
  10. package/dist/bootstrap/pin.json +119 -0
  11. package/dist/bootstrap/zip.js +258 -0
  12. package/dist/cli.js +200 -0
  13. package/dist/confine.js +244 -0
  14. package/dist/content/docs/en/cli.md +768 -0
  15. package/dist/content/docs/en/creator-space.md +78 -0
  16. package/dist/content/docs/en/dev-channel.md +130 -0
  17. package/dist/content/docs/en/elements.md +469 -0
  18. package/dist/content/docs/en/forms.md +328 -0
  19. package/dist/content/docs/en/guide.md +252 -0
  20. package/dist/content/docs/en/index.md +85 -0
  21. package/dist/content/docs/en/limits.md +241 -0
  22. package/dist/content/docs/en/logic.md +446 -0
  23. package/dist/content/docs/en/manifest.md +388 -0
  24. package/dist/content/docs/en/mcp.md +165 -0
  25. package/dist/content/docs/en/package.md +131 -0
  26. package/dist/content/docs/en/publish-a-widget.md +94 -0
  27. package/dist/content/docs/en/publish-keys.md +95 -0
  28. package/dist/content/docs/en/publishing.md +167 -0
  29. package/dist/content/docs/en/reference.md +55 -0
  30. package/dist/content/docs/en/result-shapes.md +332 -0
  31. package/dist/content/docs/en/sdk.md +295 -0
  32. package/dist/content/docs/en/security.md +115 -0
  33. package/dist/content/docs/en/service-reference.md +344 -0
  34. package/dist/content/docs/en/services.md +517 -0
  35. package/dist/content/docs/en/style-properties.md +270 -0
  36. package/dist/content/docs/en/style.md +266 -0
  37. package/dist/content/docs/en/testing.md +400 -0
  38. package/dist/content/docs/en/verified-domain.md +46 -0
  39. package/dist/content/docs/en/view.md +338 -0
  40. package/dist/content/docs/en/widget-listing.md +65 -0
  41. package/dist/content/docs/en/widget-review.md +57 -0
  42. package/dist/content/docs/en/widget-statistics.md +134 -0
  43. package/dist/content/docs/en/widgets.md +76 -0
  44. package/dist/content/docs/fr/cli.md +802 -0
  45. package/dist/content/docs/fr/creator-space.md +83 -0
  46. package/dist/content/docs/fr/dev-channel.md +140 -0
  47. package/dist/content/docs/fr/elements.md +475 -0
  48. package/dist/content/docs/fr/forms.md +341 -0
  49. package/dist/content/docs/fr/guide.md +263 -0
  50. package/dist/content/docs/fr/index.md +90 -0
  51. package/dist/content/docs/fr/limits.md +244 -0
  52. package/dist/content/docs/fr/logic.md +461 -0
  53. package/dist/content/docs/fr/manifest.md +401 -0
  54. package/dist/content/docs/fr/mcp.md +173 -0
  55. package/dist/content/docs/fr/package.md +139 -0
  56. package/dist/content/docs/fr/publish-a-widget.md +101 -0
  57. package/dist/content/docs/fr/publish-keys.md +97 -0
  58. package/dist/content/docs/fr/publishing.md +177 -0
  59. package/dist/content/docs/fr/reference.md +59 -0
  60. package/dist/content/docs/fr/result-shapes.md +333 -0
  61. package/dist/content/docs/fr/sdk.md +301 -0
  62. package/dist/content/docs/fr/security.md +129 -0
  63. package/dist/content/docs/fr/service-reference.md +346 -0
  64. package/dist/content/docs/fr/services.md +540 -0
  65. package/dist/content/docs/fr/style-properties.md +272 -0
  66. package/dist/content/docs/fr/style.md +281 -0
  67. package/dist/content/docs/fr/testing.md +417 -0
  68. package/dist/content/docs/fr/verified-domain.md +49 -0
  69. package/dist/content/docs/fr/view.md +352 -0
  70. package/dist/content/docs/fr/widget-listing.md +69 -0
  71. package/dist/content/docs/fr/widget-review.md +59 -0
  72. package/dist/content/docs/fr/widget-statistics.md +141 -0
  73. package/dist/content/docs/fr/widgets.md +81 -0
  74. package/dist/content/examples/clock/LICENSE +21 -0
  75. package/dist/content/examples/clock/README.md +75 -0
  76. package/dist/content/examples/clock/listing.json +19 -0
  77. package/dist/content/examples/clock/locales/en.json +4 -0
  78. package/dist/content/examples/clock/locales/fr.json +4 -0
  79. package/dist/content/examples/clock/logic.ts +85 -0
  80. package/dist/content/examples/clock/manifest.json +44 -0
  81. package/dist/content/examples/clock/package.json +16 -0
  82. package/dist/content/examples/clock/style.ocss +26 -0
  83. package/dist/content/examples/clock/tests/hidden.scenario.json +68 -0
  84. package/dist/content/examples/clock/tests/load-logic.mjs +22 -0
  85. package/dist/content/examples/clock/tests/logic.test.mjs +89 -0
  86. package/dist/content/examples/clock/tests/options.scenario.json +108 -0
  87. package/dist/content/examples/clock/tests/preview.scenario.json +25 -0
  88. package/dist/content/examples/clock/tests/render.scenario.json +94 -0
  89. package/dist/content/examples/clock/tests/summer-time.scenario.json +74 -0
  90. package/dist/content/examples/clock/tests/ticks.scenario.json +120 -0
  91. package/dist/content/examples/clock/tsconfig.json +16 -0
  92. package/dist/content/examples/clock/view.ocml +8 -0
  93. package/dist/content/examples/countdown/LICENSE +20 -0
  94. package/dist/content/examples/countdown/listing.json +10 -0
  95. package/dist/content/examples/countdown/locales/en.json +15 -0
  96. package/dist/content/examples/countdown/locales/fr.json +15 -0
  97. package/dist/content/examples/countdown/logic.ts +179 -0
  98. package/dist/content/examples/countdown/manifest.json +34 -0
  99. package/dist/content/examples/countdown/package.json +15 -0
  100. package/dist/content/examples/countdown/style.ocss +94 -0
  101. package/dist/content/examples/countdown/tsconfig.json +16 -0
  102. package/dist/content/examples/countdown/view.ocml +42 -0
  103. package/dist/content/examples/fps/LICENSE +21 -0
  104. package/dist/content/examples/fps/README.md +64 -0
  105. package/dist/content/examples/fps/listing.json +19 -0
  106. package/dist/content/examples/fps/locales/en.json +11 -0
  107. package/dist/content/examples/fps/locales/fr.json +11 -0
  108. package/dist/content/examples/fps/logic.ts +64 -0
  109. package/dist/content/examples/fps/manifest.json +30 -0
  110. package/dist/content/examples/fps/package.json +16 -0
  111. package/dist/content/examples/fps/style.ocss +31 -0
  112. package/dist/content/examples/fps/submission.json +8 -0
  113. package/dist/content/examples/fps/tests/label.scenario.json +63 -0
  114. package/dist/content/examples/fps/tests/load-logic.mjs +22 -0
  115. package/dist/content/examples/fps/tests/logic.test.mjs +60 -0
  116. package/dist/content/examples/fps/tests/preview.scenario.json +29 -0
  117. package/dist/content/examples/fps/tests/refused.scenario.json +20 -0
  118. package/dist/content/examples/fps/tests/render.scenario.json +96 -0
  119. package/dist/content/examples/fps/tests/stale.scenario.json +91 -0
  120. package/dist/content/examples/fps/tests/unavailable.scenario.json +111 -0
  121. package/dist/content/examples/fps/tsconfig.json +16 -0
  122. package/dist/content/examples/fps/view.ocml +11 -0
  123. package/dist/content/examples/media/LICENSE +21 -0
  124. package/dist/content/examples/media/README.md +147 -0
  125. package/dist/content/examples/media/listing.json +19 -0
  126. package/dist/content/examples/media/locales/en.json +11 -0
  127. package/dist/content/examples/media/locales/fr.json +11 -0
  128. package/dist/content/examples/media/logic.ts +191 -0
  129. package/dist/content/examples/media/manifest.json +35 -0
  130. package/dist/content/examples/media/package.json +16 -0
  131. package/dist/content/examples/media/style.ocss +148 -0
  132. package/dist/content/examples/media/submission.json +12 -0
  133. package/dist/content/examples/media/tests/covers.scenario.json +144 -0
  134. package/dist/content/examples/media/tests/gesture.scenario.json +173 -0
  135. package/dist/content/examples/media/tests/load-logic.mjs +22 -0
  136. package/dist/content/examples/media/tests/logic.test.mjs +176 -0
  137. package/dist/content/examples/media/tests/marquee.scenario.json +127 -0
  138. package/dist/content/examples/media/tests/preview.scenario.json +52 -0
  139. package/dist/content/examples/media/tests/read-only.scenario.json +53 -0
  140. package/dist/content/examples/media/tests/refused.scenario.json +22 -0
  141. package/dist/content/examples/media/tests/refused.test.mjs +31 -0
  142. package/dist/content/examples/media/tests/render.scenario.json +136 -0
  143. package/dist/content/examples/media/tests/states.scenario.json +257 -0
  144. package/dist/content/examples/media/tests/unavailable.scenario.json +120 -0
  145. package/dist/content/examples/media/tsconfig.json +16 -0
  146. package/dist/content/examples/media/view.ocml +41 -0
  147. package/dist/content/examples/notes/LICENSE +21 -0
  148. package/dist/content/examples/notes/README.md +219 -0
  149. package/dist/content/examples/notes/listing.json +19 -0
  150. package/dist/content/examples/notes/locales/en.json +25 -0
  151. package/dist/content/examples/notes/locales/fr.json +25 -0
  152. package/dist/content/examples/notes/logic.ts +799 -0
  153. package/dist/content/examples/notes/manifest.json +40 -0
  154. package/dist/content/examples/notes/package.json +16 -0
  155. package/dist/content/examples/notes/style.ocss +469 -0
  156. package/dist/content/examples/notes/submission.json +12 -0
  157. package/dist/content/examples/notes/tests/checks.scenario.json +237 -0
  158. package/dist/content/examples/notes/tests/create.scenario.json +260 -0
  159. package/dist/content/examples/notes/tests/delete.scenario.json +342 -0
  160. package/dist/content/examples/notes/tests/drafts.scenario.json +714 -0
  161. package/dist/content/examples/notes/tests/editor.scenario.json +232 -0
  162. package/dist/content/examples/notes/tests/frame.scenario.json +93 -0
  163. package/dist/content/examples/notes/tests/interactive.scenario.json +344 -0
  164. package/dist/content/examples/notes/tests/limits.scenario.json +1351 -0
  165. package/dist/content/examples/notes/tests/load-logic.mjs +22 -0
  166. package/dist/content/examples/notes/tests/logic.test.mjs +514 -0
  167. package/dist/content/examples/notes/tests/preview.scenario.json +99 -0
  168. package/dist/content/examples/notes/tests/read-only.scenario.json +105 -0
  169. package/dist/content/examples/notes/tests/refused.scenario.json +33 -0
  170. package/dist/content/examples/notes/tests/save.scenario.json +378 -0
  171. package/dist/content/examples/notes/tests/states.scenario.json +640 -0
  172. package/dist/content/examples/notes/tests/typing.scenario.json +562 -0
  173. package/dist/content/examples/notes/tests/unavailable.scenario.json +246 -0
  174. package/dist/content/examples/notes/tsconfig.json +16 -0
  175. package/dist/content/examples/notes/view.ocml +154 -0
  176. package/dist/content/examples/performance/LICENSE +21 -0
  177. package/dist/content/examples/performance/README.md +134 -0
  178. package/dist/content/examples/performance/listing.json +19 -0
  179. package/dist/content/examples/performance/locales/en.json +20 -0
  180. package/dist/content/examples/performance/locales/fr.json +20 -0
  181. package/dist/content/examples/performance/logic.ts +210 -0
  182. package/dist/content/examples/performance/manifest.json +83 -0
  183. package/dist/content/examples/performance/package.json +16 -0
  184. package/dist/content/examples/performance/style.ocss +75 -0
  185. package/dist/content/examples/performance/submission.json +12 -0
  186. package/dist/content/examples/performance/tests/fahrenheit.scenario.json +110 -0
  187. package/dist/content/examples/performance/tests/horizontal.scenario.json +96 -0
  188. package/dist/content/examples/performance/tests/load-logic.mjs +22 -0
  189. package/dist/content/examples/performance/tests/logic.test.mjs +226 -0
  190. package/dist/content/examples/performance/tests/menu.scenario.json +170 -0
  191. package/dist/content/examples/performance/tests/no-fps-source.scenario.json +47 -0
  192. package/dist/content/examples/performance/tests/preview.scenario.json +45 -0
  193. package/dist/content/examples/performance/tests/refused.scenario.json +20 -0
  194. package/dist/content/examples/performance/tests/refused.test.mjs +19 -0
  195. package/dist/content/examples/performance/tests/region.scenario.json +89 -0
  196. package/dist/content/examples/performance/tests/render.scenario.json +112 -0
  197. package/dist/content/examples/performance/tests/sources.scenario.json +151 -0
  198. package/dist/content/examples/performance/tests/stale.scenario.json +111 -0
  199. package/dist/content/examples/performance/tests/telemetry-only.scenario.json +44 -0
  200. package/dist/content/examples/performance/tests/thresholds.scenario.json +147 -0
  201. package/dist/content/examples/performance/tests/waiting.scenario.json +105 -0
  202. package/dist/content/examples/performance/tsconfig.json +16 -0
  203. package/dist/content/examples/performance/view.ocml +18 -0
  204. package/dist/content/examples/playervox-journal/LICENSE +21 -0
  205. package/dist/content/examples/playervox-journal/README.md +150 -0
  206. package/dist/content/examples/playervox-journal/listing.json +19 -0
  207. package/dist/content/examples/playervox-journal/locales/en.json +25 -0
  208. package/dist/content/examples/playervox-journal/locales/fr.json +25 -0
  209. package/dist/content/examples/playervox-journal/logic.ts +556 -0
  210. package/dist/content/examples/playervox-journal/manifest.json +19 -0
  211. package/dist/content/examples/playervox-journal/package.json +16 -0
  212. package/dist/content/examples/playervox-journal/style.ocss +142 -0
  213. package/dist/content/examples/playervox-journal/submission.json +12 -0
  214. package/dist/content/examples/playervox-journal/tests/changes.scenario.json +482 -0
  215. package/dist/content/examples/playervox-journal/tests/delete.scenario.json +432 -0
  216. package/dist/content/examples/playervox-journal/tests/frame.scenario.json +169 -0
  217. package/dist/content/examples/playervox-journal/tests/load-logic.mjs +22 -0
  218. package/dist/content/examples/playervox-journal/tests/logic.test.mjs +349 -0
  219. package/dist/content/examples/playervox-journal/tests/paging.scenario.json +428 -0
  220. package/dist/content/examples/playervox-journal/tests/preview.scenario.json +184 -0
  221. package/dist/content/examples/playervox-journal/tests/read-only.scenario.json +157 -0
  222. package/dist/content/examples/playervox-journal/tests/refused.scenario.json +39 -0
  223. package/dist/content/examples/playervox-journal/tests/scale.scenario.json +286 -0
  224. package/dist/content/examples/playervox-journal/tests/signed-out.scenario.json +134 -0
  225. package/dist/content/examples/playervox-journal/tests/states.scenario.json +741 -0
  226. package/dist/content/examples/playervox-journal/tests/unavailable.scenario.json +183 -0
  227. package/dist/content/examples/playervox-journal/tsconfig.json +16 -0
  228. package/dist/content/examples/playervox-journal/view.ocml +52 -0
  229. package/dist/content/examples/playervox-rating/LICENSE +21 -0
  230. package/dist/content/examples/playervox-rating/README.md +166 -0
  231. package/dist/content/examples/playervox-rating/listing.json +19 -0
  232. package/dist/content/examples/playervox-rating/locales/en.json +35 -0
  233. package/dist/content/examples/playervox-rating/locales/fr.json +35 -0
  234. package/dist/content/examples/playervox-rating/logic.ts +581 -0
  235. package/dist/content/examples/playervox-rating/manifest.json +19 -0
  236. package/dist/content/examples/playervox-rating/package.json +16 -0
  237. package/dist/content/examples/playervox-rating/style.ocss +333 -0
  238. package/dist/content/examples/playervox-rating/submission.json +12 -0
  239. package/dist/content/examples/playervox-rating/tests/account.scenario.json +125 -0
  240. package/dist/content/examples/playervox-rating/tests/bounds.scenario.json +73 -0
  241. package/dist/content/examples/playervox-rating/tests/draft.scenario.json +199 -0
  242. package/dist/content/examples/playervox-rating/tests/form.scenario.json +142 -0
  243. package/dist/content/examples/playervox-rating/tests/load-logic.mjs +22 -0
  244. package/dist/content/examples/playervox-rating/tests/logic.test.mjs +346 -0
  245. package/dist/content/examples/playervox-rating/tests/offline.scenario.json +142 -0
  246. package/dist/content/examples/playervox-rating/tests/preview.scenario.json +44 -0
  247. package/dist/content/examples/playervox-rating/tests/publish-keyboard.scenario.json +221 -0
  248. package/dist/content/examples/playervox-rating/tests/publish-pointer.scenario.json +314 -0
  249. package/dist/content/examples/playervox-rating/tests/read-only.scenario.json +70 -0
  250. package/dist/content/examples/playervox-rating/tests/refused.scenario.json +45 -0
  251. package/dist/content/examples/playervox-rating/tests/states.scenario.json +362 -0
  252. package/dist/content/examples/playervox-rating/tsconfig.json +16 -0
  253. package/dist/content/examples/playervox-rating/view.ocml +62 -0
  254. package/dist/content/examples/playervox-reviews/LICENSE +21 -0
  255. package/dist/content/examples/playervox-reviews/README.md +194 -0
  256. package/dist/content/examples/playervox-reviews/listing.json +19 -0
  257. package/dist/content/examples/playervox-reviews/locales/en.json +32 -0
  258. package/dist/content/examples/playervox-reviews/locales/fr.json +32 -0
  259. package/dist/content/examples/playervox-reviews/logic.ts +822 -0
  260. package/dist/content/examples/playervox-reviews/manifest.json +30 -0
  261. package/dist/content/examples/playervox-reviews/package.json +16 -0
  262. package/dist/content/examples/playervox-reviews/style.ocss +260 -0
  263. package/dist/content/examples/playervox-reviews/submission.json +8 -0
  264. package/dist/content/examples/playervox-reviews/tests/account.scenario.json +249 -0
  265. package/dist/content/examples/playervox-reviews/tests/bounds.scenario.json +336 -0
  266. package/dist/content/examples/playervox-reviews/tests/changes.scenario.json +515 -0
  267. package/dist/content/examples/playervox-reviews/tests/filter.scenario.json +400 -0
  268. package/dist/content/examples/playervox-reviews/tests/frame.scenario.json +185 -0
  269. package/dist/content/examples/playervox-reviews/tests/keyboard.scenario.json +188 -0
  270. package/dist/content/examples/playervox-reviews/tests/load-logic.mjs +22 -0
  271. package/dist/content/examples/playervox-reviews/tests/logic.test.mjs +559 -0
  272. package/dist/content/examples/playervox-reviews/tests/offline-start.scenario.json +152 -0
  273. package/dist/content/examples/playervox-reviews/tests/offline.scenario.json +311 -0
  274. package/dist/content/examples/playervox-reviews/tests/paging.scenario.json +497 -0
  275. package/dist/content/examples/playervox-reviews/tests/preview.scenario.json +286 -0
  276. package/dist/content/examples/playervox-reviews/tests/reading.scenario.json +591 -0
  277. package/dist/content/examples/playervox-reviews/tests/refused.scenario.json +54 -0
  278. package/dist/content/examples/playervox-reviews/tests/scale.scenario.json +303 -0
  279. package/dist/content/examples/playervox-reviews/tests/states.scenario.json +700 -0
  280. package/dist/content/examples/playervox-reviews/tests/unavailable.scenario.json +192 -0
  281. package/dist/content/examples/playervox-reviews/tsconfig.json +16 -0
  282. package/dist/content/examples/playervox-reviews/view.ocml +110 -0
  283. package/dist/content/examples/playervox-score/LICENSE +21 -0
  284. package/dist/content/examples/playervox-score/README.md +131 -0
  285. package/dist/content/examples/playervox-score/listing.json +19 -0
  286. package/dist/content/examples/playervox-score/locales/en.json +23 -0
  287. package/dist/content/examples/playervox-score/locales/fr.json +23 -0
  288. package/dist/content/examples/playervox-score/logic.ts +430 -0
  289. package/dist/content/examples/playervox-score/manifest.json +47 -0
  290. package/dist/content/examples/playervox-score/package.json +16 -0
  291. package/dist/content/examples/playervox-score/style.ocss +247 -0
  292. package/dist/content/examples/playervox-score/submission.json +8 -0
  293. package/dist/content/examples/playervox-score/tests/grades.scenario.json +279 -0
  294. package/dist/content/examples/playervox-score/tests/load-logic.mjs +22 -0
  295. package/dist/content/examples/playervox-score/tests/logic.test.mjs +229 -0
  296. package/dist/content/examples/playervox-score/tests/options.scenario.json +302 -0
  297. package/dist/content/examples/playervox-score/tests/preview.scenario.json +68 -0
  298. package/dist/content/examples/playervox-score/tests/pulse.scenario.json +157 -0
  299. package/dist/content/examples/playervox-score/tests/refused.scenario.json +21 -0
  300. package/dist/content/examples/playervox-score/tests/signed-out.scenario.json +46 -0
  301. package/dist/content/examples/playervox-score/tests/states.scenario.json +450 -0
  302. package/dist/content/examples/playervox-score/tests/unavailable.scenario.json +84 -0
  303. package/dist/content/examples/playervox-score/tsconfig.json +16 -0
  304. package/dist/content/examples/playervox-score/view.ocml +51 -0
  305. package/dist/content/examples/session/LICENSE +21 -0
  306. package/dist/content/examples/session/README.md +57 -0
  307. package/dist/content/examples/session/listing.json +19 -0
  308. package/dist/content/examples/session/locales/en.json +3 -0
  309. package/dist/content/examples/session/locales/fr.json +3 -0
  310. package/dist/content/examples/session/logic.ts +26 -0
  311. package/dist/content/examples/session/manifest.json +19 -0
  312. package/dist/content/examples/session/package.json +16 -0
  313. package/dist/content/examples/session/style.ocss +16 -0
  314. package/dist/content/examples/session/submission.json +8 -0
  315. package/dist/content/examples/session/tests/hidden.scenario.json +45 -0
  316. package/dist/content/examples/session/tests/load-logic.mjs +22 -0
  317. package/dist/content/examples/session/tests/logic.test.mjs +37 -0
  318. package/dist/content/examples/session/tests/preview.scenario.json +28 -0
  319. package/dist/content/examples/session/tests/refused.scenario.json +20 -0
  320. package/dist/content/examples/session/tests/refused.test.mjs +16 -0
  321. package/dist/content/examples/session/tests/render.scenario.json +95 -0
  322. package/dist/content/examples/session/tests/ticks.scenario.json +139 -0
  323. package/dist/content/examples/session/tests/unknown.scenario.json +37 -0
  324. package/dist/content/examples/session/tsconfig.json +16 -0
  325. package/dist/content/examples/session/view.ocml +11 -0
  326. package/dist/content/examples/stopwatch/LICENSE +21 -0
  327. package/dist/content/examples/stopwatch/README.md +99 -0
  328. package/dist/content/examples/stopwatch/listing.json +19 -0
  329. package/dist/content/examples/stopwatch/locales/en.json +8 -0
  330. package/dist/content/examples/stopwatch/locales/fr.json +8 -0
  331. package/dist/content/examples/stopwatch/logic.ts +93 -0
  332. package/dist/content/examples/stopwatch/manifest.json +19 -0
  333. package/dist/content/examples/stopwatch/package.json +16 -0
  334. package/dist/content/examples/stopwatch/style.ocss +54 -0
  335. package/dist/content/examples/stopwatch/submission.json +12 -0
  336. package/dist/content/examples/stopwatch/tests/errors.scenario.json +121 -0
  337. package/dist/content/examples/stopwatch/tests/gesture.scenario.json +154 -0
  338. package/dist/content/examples/stopwatch/tests/hidden.scenario.json +57 -0
  339. package/dist/content/examples/stopwatch/tests/hundredths.scenario.json +173 -0
  340. package/dist/content/examples/stopwatch/tests/load-logic.mjs +22 -0
  341. package/dist/content/examples/stopwatch/tests/logic.test.mjs +99 -0
  342. package/dist/content/examples/stopwatch/tests/preview.scenario.json +36 -0
  343. package/dist/content/examples/stopwatch/tests/read-only.scenario.json +42 -0
  344. package/dist/content/examples/stopwatch/tests/read-only.test.mjs +18 -0
  345. package/dist/content/examples/stopwatch/tests/refused.scenario.json +22 -0
  346. package/dist/content/examples/stopwatch/tests/refused.test.mjs +18 -0
  347. package/dist/content/examples/stopwatch/tests/render.scenario.json +105 -0
  348. package/dist/content/examples/stopwatch/tests/states.scenario.json +246 -0
  349. package/dist/content/examples/stopwatch/tests/unknown.scenario.json +40 -0
  350. package/dist/content/examples/stopwatch/tsconfig.json +16 -0
  351. package/dist/content/examples/stopwatch/view.ocml +29 -0
  352. package/dist/content/examples/twitch-chat/LICENSE +21 -0
  353. package/dist/content/examples/twitch-chat/README.md +283 -0
  354. package/dist/content/examples/twitch-chat/listing.json +19 -0
  355. package/dist/content/examples/twitch-chat/locales/en.json +52 -0
  356. package/dist/content/examples/twitch-chat/locales/fr.json +52 -0
  357. package/dist/content/examples/twitch-chat/logic.ts +887 -0
  358. package/dist/content/examples/twitch-chat/manifest.json +33 -0
  359. package/dist/content/examples/twitch-chat/package.json +16 -0
  360. package/dist/content/examples/twitch-chat/style.ocss +544 -0
  361. package/dist/content/examples/twitch-chat/submission.json +12 -0
  362. package/dist/content/examples/twitch-chat/tests/account.scenario.json +325 -0
  363. package/dist/content/examples/twitch-chat/tests/burst.scenario.json +1866 -0
  364. package/dist/content/examples/twitch-chat/tests/channel-refused.scenario.json +372 -0
  365. package/dist/content/examples/twitch-chat/tests/channel.scenario.json +480 -0
  366. package/dist/content/examples/twitch-chat/tests/draft.scenario.json +279 -0
  367. package/dist/content/examples/twitch-chat/tests/fade.scenario.json +269 -0
  368. package/dist/content/examples/twitch-chat/tests/failures.scenario.json +420 -0
  369. package/dist/content/examples/twitch-chat/tests/favorites.scenario.json +376 -0
  370. package/dist/content/examples/twitch-chat/tests/frame.scenario.json +152 -0
  371. package/dist/content/examples/twitch-chat/tests/lifetime.scenario.json +186 -0
  372. package/dist/content/examples/twitch-chat/tests/load-logic.mjs +22 -0
  373. package/dist/content/examples/twitch-chat/tests/logic.test.mjs +1053 -0
  374. package/dist/content/examples/twitch-chat/tests/passive.scenario.json +536 -0
  375. package/dist/content/examples/twitch-chat/tests/preview.scenario.json +147 -0
  376. package/dist/content/examples/twitch-chat/tests/read-only.scenario.json +202 -0
  377. package/dist/content/examples/twitch-chat/tests/read-only.test.mjs +63 -0
  378. package/dist/content/examples/twitch-chat/tests/refused.scenario.json +65 -0
  379. package/dist/content/examples/twitch-chat/tests/refused.test.mjs +43 -0
  380. package/dist/content/examples/twitch-chat/tests/render.scenario.json +347 -0
  381. package/dist/content/examples/twitch-chat/tests/reply.scenario.json +384 -0
  382. package/dist/content/examples/twitch-chat/tests/send-refused.scenario.json +383 -0
  383. package/dist/content/examples/twitch-chat/tests/send.scenario.json +300 -0
  384. package/dist/content/examples/twitch-chat/tests/states.scenario.json +455 -0
  385. package/dist/content/examples/twitch-chat/tests/unavailable.scenario.json +256 -0
  386. package/dist/content/examples/twitch-chat/tests/unread.scenario.json +997 -0
  387. package/dist/content/examples/twitch-chat/tsconfig.json +16 -0
  388. package/dist/content/examples/twitch-chat/view.ocml +141 -0
  389. package/dist/content/examples/warframe-market/LICENSE +21 -0
  390. package/dist/content/examples/warframe-market/README.md +176 -0
  391. package/dist/content/examples/warframe-market/listing.json +19 -0
  392. package/dist/content/examples/warframe-market/locales/en.json +44 -0
  393. package/dist/content/examples/warframe-market/locales/fr.json +44 -0
  394. package/dist/content/examples/warframe-market/logic.ts +1265 -0
  395. package/dist/content/examples/warframe-market/manifest.json +92 -0
  396. package/dist/content/examples/warframe-market/package.json +16 -0
  397. package/dist/content/examples/warframe-market/style.ocss +422 -0
  398. package/dist/content/examples/warframe-market/submission.json +24 -0
  399. package/dist/content/examples/warframe-market/tests/bounds.scenario.json +60 -0
  400. package/dist/content/examples/warframe-market/tests/cached.scenario.json +70 -0
  401. package/dist/content/examples/warframe-market/tests/detail.scenario.json +251 -0
  402. package/dist/content/examples/warframe-market/tests/errors.scenario.json +105 -0
  403. package/dist/content/examples/warframe-market/tests/fixtures.json +295 -0
  404. package/dist/content/examples/warframe-market/tests/load-logic.mjs +22 -0
  405. package/dist/content/examples/warframe-market/tests/logic.test.mjs +505 -0
  406. package/dist/content/examples/warframe-market/tests/no-clipboard.scenario.json +104 -0
  407. package/dist/content/examples/warframe-market/tests/passive.scenario.json +87 -0
  408. package/dist/content/examples/warframe-market/tests/preview.scenario.json +146 -0
  409. package/dist/content/examples/warframe-market/tests/refresh.scenario.json +56 -0
  410. package/dist/content/examples/warframe-market/tests/refused.scenario.json +74 -0
  411. package/dist/content/examples/warframe-market/tests/render.scenario.json +210 -0
  412. package/dist/content/examples/warframe-market/tests/search.scenario.json +189 -0
  413. package/dist/content/examples/warframe-market/tests/unavailable.scenario.json +54 -0
  414. package/dist/content/examples/warframe-market/tsconfig.json +16 -0
  415. package/dist/content/examples/warframe-market/view.ocml +116 -0
  416. package/dist/content/examples/weather/LICENSE +20 -0
  417. package/dist/content/examples/weather/listing.json +10 -0
  418. package/dist/content/examples/weather/locales/en.json +9 -0
  419. package/dist/content/examples/weather/locales/fr.json +9 -0
  420. package/dist/content/examples/weather/logic.ts +62 -0
  421. package/dist/content/examples/weather/manifest.json +51 -0
  422. package/dist/content/examples/weather/package.json +15 -0
  423. package/dist/content/examples/weather/style.ocss +29 -0
  424. package/dist/content/examples/weather/submission.json +17 -0
  425. package/dist/content/examples/weather/tsconfig.json +16 -0
  426. package/dist/content/examples/weather/view.ocml +12 -0
  427. package/dist/content/index.json +3705 -0
  428. package/dist/content/reference/errors.json +1924 -0
  429. package/dist/content/reference/limits.json +1154 -0
  430. package/dist/content/reference/manifest.md +223 -0
  431. package/dist/content/reference/permissions.json +413 -0
  432. package/dist/content/templates/blank/locales/en.json +3 -0
  433. package/dist/content/templates/blank/locales/fr.json +3 -0
  434. package/dist/content/templates/blank/logic.ts +7 -0
  435. package/dist/content/templates/blank/manifest.json +16 -0
  436. package/dist/content/templates/blank/style.ocss +10 -0
  437. package/dist/content/templates/blank/tests/example.scenario.json +10 -0
  438. package/dist/content/templates/blank/view.ocml +6 -0
  439. package/dist/content/templates/chart/locales/en.json +3 -0
  440. package/dist/content/templates/chart/locales/fr.json +3 -0
  441. package/dist/content/templates/chart/logic.ts +27 -0
  442. package/dist/content/templates/chart/manifest.json +16 -0
  443. package/dist/content/templates/chart/style.ocss +27 -0
  444. package/dist/content/templates/chart/tests/example.scenario.json +10 -0
  445. package/dist/content/templates/chart/view.ocml +10 -0
  446. package/dist/content/templates/counter/locales/en.json +4 -0
  447. package/dist/content/templates/counter/locales/fr.json +4 -0
  448. package/dist/content/templates/counter/logic.ts +20 -0
  449. package/dist/content/templates/counter/manifest.json +16 -0
  450. package/dist/content/templates/counter/style.ocss +26 -0
  451. package/dist/content/templates/counter/tests/example.scenario.json +11 -0
  452. package/dist/content/templates/counter/view.ocml +13 -0
  453. package/dist/content/templates/list/locales/en.json +8 -0
  454. package/dist/content/templates/list/locales/fr.json +8 -0
  455. package/dist/content/templates/list/logic.ts +35 -0
  456. package/dist/content/templates/list/manifest.json +16 -0
  457. package/dist/content/templates/list/style.ocss +42 -0
  458. package/dist/content/templates/list/tests/example.scenario.json +10 -0
  459. package/dist/content/templates/list/view.ocml +17 -0
  460. package/dist/content/templates/shared/LICENSE +20 -0
  461. package/dist/content/templates/shared/package.json +14 -0
  462. package/dist/content/templates/shared/tsconfig.json +16 -0
  463. package/dist/content.js +68 -0
  464. package/dist/env.js +156 -0
  465. package/dist/fingerprint.js +32 -0
  466. package/dist/hidden.js +47 -0
  467. package/dist/ids.js +27 -0
  468. package/dist/index.js +82 -0
  469. package/dist/knowledge.js +222 -0
  470. package/dist/npm.js +149 -0
  471. package/dist/prompts.js +47 -0
  472. package/dist/resources.js +82 -0
  473. package/dist/run.js +120 -0
  474. package/dist/server.js +43 -0
  475. package/dist/session.js +205 -0
  476. package/dist/sources.js +218 -0
  477. package/dist/text.js +80 -0
  478. package/dist/texts.js +44 -0
  479. package/dist/tools/audit.js +105 -0
  480. package/dist/tools/build.js +453 -0
  481. package/dist/tools/common.js +122 -0
  482. package/dist/tools/docs.js +196 -0
  483. package/dist/tools/preview.js +87 -0
  484. package/dist/tools/project.js +183 -0
  485. package/dist/tools/setup.js +146 -0
  486. package/dist/tools/submission.js +369 -0
  487. package/dist/version.js +3 -0
  488. package/npm-shrinkwrap.json +402 -0
  489. package/package.json +50 -4
@@ -0,0 +1,661 @@
1
+ // The rules of the audit. Each one reads the widget as data and reports
2
+ // findings with a severity, where, why, how to fix it, and an example from
3
+ // a reference widget. Thresholds come from the documented limits
4
+ // (content/reference/limits.json), never from copies.
5
+ //
6
+ // Severities: high = a real risk or a refusal at review; medium = more
7
+ // authority or cost than the widget seems to need; low = advice. Patterns
8
+ // the reference widgets use on purpose are not reported: timers left
9
+ // running while the widget is hidden (the host does not tick them),
10
+ // one-shot retries and watchdogs, slice-by-slice parsing.
11
+ import { limit, permissionsTable } from "../content.js";
12
+ import { ID_FORMS, isPlaceholderId, isReservedId } from "../ids.js";
13
+ import { callArguments, calls, enclosing, evaluate, functions, numericConstants, } from "./lexer.js";
14
+ import { infiniteAnimations, viewFacts } from "./project.js";
15
+ const GESTURE_EVENTS = new Set(["activate", "contextmenu", "keydown", "input", "change", "submit"]);
16
+ const KIB = 1024;
17
+ function add(context, finding) {
18
+ context.findings.push(finding);
19
+ }
20
+ const permissionsOf = (project) => project.manifest?.permissions ?? {};
21
+ /** True when the logic or the view uses the dotted service name. */
22
+ function usesService(context, service) {
23
+ const formIntent = /^form (.+)$/.exec(service)?.[1];
24
+ if (formIntent)
25
+ return context.view.intents.some((intent) => intent.name === formIntent);
26
+ const path = service.split(".");
27
+ if (calls(context.tokens, path).length > 0)
28
+ return true;
29
+ // `call("x.y")`, `subscribe("x.y", …)` or a member reference passed around.
30
+ if (context.strings.includes(service))
31
+ return true;
32
+ const joined = context.tokens
33
+ .map((token) => (token.kind === "identifier" || token.value === "." ? token.value : " "))
34
+ .join("");
35
+ return joined.split(" ").some((chain) => chain === service || chain.startsWith(`${service}.`));
36
+ }
37
+ // ── Security ─────────────────────────────────────────────────────────────
38
+ function unusedAuthority(context) {
39
+ const permissions = permissionsOf(context.project);
40
+ const table = permissionsTable();
41
+ const capabilities = Array.isArray(permissions.capabilities)
42
+ ? permissions.capabilities
43
+ : [];
44
+ for (const capability of capabilities) {
45
+ const services = table.capabilities[capability]?.services ?? [];
46
+ if (services.length > 0 && !services.some((entry) => usesService(context, entry.service))) {
47
+ add(context, {
48
+ rule: "unused-capability",
49
+ area: "security",
50
+ severity: "high",
51
+ file: "manifest.json",
52
+ line: null,
53
+ message: `The manifest asks for the capability ${capability}, but the widget never uses it (${services.map((s) => s.service).join(", ")}).`,
54
+ fix: `Remove ${capability} from permissions.capabilities: every capability needs the user's consent and widens what the widget may reach.`,
55
+ });
56
+ }
57
+ }
58
+ const checks = [
59
+ {
60
+ key: "storage",
61
+ on: permissions.storage === true,
62
+ services: ["storage.get", "storage.set", "storage.remove", "storage.keys"],
63
+ name: "storage",
64
+ },
65
+ {
66
+ key: "clipboardWrite",
67
+ on: permissions.clipboardWrite === true,
68
+ services: ["clipboard.writeText"],
69
+ name: "clipboardWrite",
70
+ },
71
+ {
72
+ key: "network",
73
+ on: Array.isArray(permissions.network) && permissions.network.length > 0,
74
+ services: ["http.fetch"],
75
+ name: "network",
76
+ },
77
+ {
78
+ key: "gameEvents",
79
+ on: Array.isArray(permissions.gameEvents) && permissions.gameEvents.length > 0,
80
+ services: ["gameEvents.subscribe"],
81
+ name: "gameEvents",
82
+ },
83
+ ];
84
+ for (const check of checks) {
85
+ if (check.on && !check.services.some((service) => usesService(context, service))) {
86
+ add(context, {
87
+ rule: "unused-permission",
88
+ area: "security",
89
+ severity: "high",
90
+ file: "manifest.json",
91
+ line: null,
92
+ message: `The manifest declares ${check.name}, but the logic never calls ${check.services.join(" or ")}.`,
93
+ fix: `Remove permissions.${check.key}. Declare only what the widget uses: each permission is shown to the user and checked at review.`,
94
+ });
95
+ }
96
+ }
97
+ }
98
+ /** The static start of a route path, before its first `{parameter}`. */
99
+ function staticPrefix(path) {
100
+ const index = path.indexOf("{");
101
+ return index < 0 ? path : path.slice(0, index);
102
+ }
103
+ /** The string or template chunks that mention `text`, with their token index. */
104
+ function literalIndexes(context, text) {
105
+ const out = [];
106
+ context.tokens.forEach((token, index) => {
107
+ if ((token.kind === "string" || token.kind === "template") && token.value.includes(text))
108
+ out.push(index);
109
+ });
110
+ return out;
111
+ }
112
+ function networkRules(context) {
113
+ const rules = permissionsOf(context.project).network ?? [];
114
+ if (!Array.isArray(rules) || rules.length === 0)
115
+ return;
116
+ const origins = new Set(rules.map((rule) => String(rule.origin ?? "")));
117
+ const defaultBound = limit("MAX_HTTP_RESPONSE_BYTES");
118
+ rules.forEach((rule, index) => {
119
+ const where = `permissions.network[${index}]`;
120
+ const origin = String(rule.origin ?? "");
121
+ const path = String(rule.path ?? "");
122
+ const method = String(rule.method ?? "GET");
123
+ const route = `${method} ${origin}${path}`;
124
+ const host = origin.replace(/^https:\/\//, "");
125
+ const prefix = staticPrefix(path);
126
+ const usesOrigin = context.strings.some((value) => value.includes(host));
127
+ const usesPath = prefix === "/" || context.strings.some((value) => value.includes(prefix.replace(/\/$/, "")));
128
+ if (!usesOrigin || !usesPath) {
129
+ add(context, {
130
+ rule: "unused-route",
131
+ area: "security",
132
+ severity: "medium",
133
+ file: "manifest.json",
134
+ line: null,
135
+ message: `${where} (${route}) does not appear in the logic: no text of the logic names ${usesOrigin ? prefix : host}.`,
136
+ fix: "Remove the routes the widget does not request. If the URL is built elsewhere, keep the route and ignore this finding.",
137
+ });
138
+ }
139
+ if (rule.maxResponseBytes === undefined) {
140
+ add(context, {
141
+ rule: "unbounded-response",
142
+ area: "security",
143
+ severity: "medium",
144
+ file: "manifest.json",
145
+ line: null,
146
+ message: `${where} (${route}) has no maxResponseBytes: responses up to ${defaultBound / KIB / KIB} MiB are accepted.`,
147
+ fix: "Set maxResponseBytes a little above the largest response this route really returns (measure it). A tight bound limits memory and what a compromised server can send.",
148
+ example: {
149
+ widget: "warframe-market",
150
+ file: "manifest.json",
151
+ find: '"path": "/v2/versions"',
152
+ },
153
+ });
154
+ }
155
+ if (method !== "GET") {
156
+ add(context, {
157
+ rule: "sends-data",
158
+ area: "security",
159
+ severity: "low",
160
+ file: "manifest.json",
161
+ line: null,
162
+ message: `${where} uses ${method}: the widget sends data to ${host}.`,
163
+ fix: "Send only what the feature needs, never the user's data without a reason, and say in listing.json what is sent and why.",
164
+ });
165
+ }
166
+ for (const [kind, params] of [
167
+ ["path", rule.pathParams],
168
+ ["query", rule.queryParams],
169
+ ]) {
170
+ for (const [name, constraint] of Object.entries(params ?? {})) {
171
+ const maxLength = Number(constraint?.maxLength ?? 0);
172
+ if (constraint?.type === "string" && maxLength > 64) {
173
+ add(context, {
174
+ rule: "broad-parameter",
175
+ area: "security",
176
+ severity: "low",
177
+ file: "manifest.json",
178
+ line: null,
179
+ message: `${where}: the ${kind} parameter ${name} accepts any text up to ${maxLength} characters.`,
180
+ fix: 'Constrain it more tightly: "enum" for a fixed set, "integer" with min and max, or "slug" with a short maxLength.',
181
+ example: { widget: "weather", file: "manifest.json", find: '"units"' },
182
+ });
183
+ }
184
+ }
185
+ }
186
+ });
187
+ if (origins.size > 2) {
188
+ add(context, {
189
+ rule: "many-origins",
190
+ area: "security",
191
+ severity: "low",
192
+ file: "manifest.json",
193
+ line: null,
194
+ message: `The widget reaches ${origins.size} different servers.`,
195
+ fix: "Each server is a party that learns when the user plays. Keep only the ones the feature needs.",
196
+ });
197
+ }
198
+ }
199
+ /** Brace depth of the token at `index` (0 = top level of the module). */
200
+ function depthAt(tokens, index) {
201
+ let depth = 0;
202
+ for (let i = 0; i < index; i += 1) {
203
+ const value = tokens[i]?.value;
204
+ if (tokens[i]?.kind !== "punctuation")
205
+ continue;
206
+ if (value === "{")
207
+ depth += 1;
208
+ else if (value === "}")
209
+ depth -= 1;
210
+ }
211
+ return depth;
212
+ }
213
+ function clipboardOutsideGesture(context) {
214
+ for (const open of calls(context.tokens, ["clipboard", "writeText"])) {
215
+ const line = context.tokens[open]?.line ?? null;
216
+ const owner = enclosing(context.ranges, open);
217
+ const isGestureHandler = (name) => [...(context.view.handlers.get(name) ?? [])].some((event) => GESTURE_EVENTS.has(event));
218
+ let confirmed = owner !== undefined && isGestureHandler(owner.name);
219
+ if (!confirmed && owner) {
220
+ // One level up: a gesture handler that calls the owner.
221
+ confirmed = context.ranges.some((range) => isGestureHandler(range.name) &&
222
+ calls(context.tokens.slice(range.start, range.end), [owner.name]).length > 0);
223
+ }
224
+ if (confirmed)
225
+ continue;
226
+ const topLevel = owner === undefined && depthAt(context.tokens, open) === 0;
227
+ add(context, {
228
+ rule: "clipboard-without-action",
229
+ area: "security",
230
+ severity: topLevel ? "high" : "medium",
231
+ file: context.logicFile,
232
+ line,
233
+ message: topLevel
234
+ ? "clipboard.writeText runs when the widget starts: it would write without the user asking."
235
+ : owner
236
+ ? `clipboard.writeText is in ${owner.name}, which no button or key of the view calls directly.`
237
+ : "clipboard.writeText is in a callback (a timer, a subscription…) that no user action calls.",
238
+ fix: "Write to the clipboard only in the handler of a user action (on:activate of a button): OverCrow refuses it otherwise (gesture_required), and the user must always know what is copied.",
239
+ example: { widget: "warframe-market", file: "logic.ts", find: "export function copy(" },
240
+ });
241
+ }
242
+ }
243
+ const SCAN_WINDOW = 4096;
244
+ const SECRET_PATTERNS = [
245
+ { name: "a private key", pattern: /-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY(?: BLOCK)?-----/ },
246
+ { name: "a GitHub token", pattern: /\bgh[pousr]_[A-Za-z0-9]{30,}/ },
247
+ { name: "an AWS access key", pattern: /\bAKIA[0-9A-Z]{16}\b/ },
248
+ { name: "a Slack token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{20,}/ },
249
+ { name: "an API key (sk-…)", pattern: /(^|[^\w-])sk-(?:proj-)?[A-Za-z0-9_-]{20,}/ },
250
+ { name: "a Google API key", pattern: /\bAIza[0-9A-Za-z_-]{35}\b/ },
251
+ { name: "an npm token", pattern: /\bnpm_[A-Za-z0-9]{36}\b/ },
252
+ { name: "a Twitch or OAuth token", pattern: /\boauth:[a-z0-9]{30}\b/ },
253
+ { name: "credentials in a URL", pattern: /\bhttps?:\/\/[^\s/:@"'`]+:[^\s/@"'`]+@[^\s"'`]+/ },
254
+ {
255
+ name: "a hard-coded secret",
256
+ pattern: /\b(?:api[_-]?key|apikey|secret|client[_-]?secret|access[_-]?token|auth[_-]?token|password|bearer)\b["']?\s*[:=]\s*["'`][A-Za-z0-9_\-./+=]{16,}["'`]/i,
257
+ },
258
+ ];
259
+ function secrets(context) {
260
+ for (const file of context.project.files) {
261
+ const name = file.path.split("/").pop() ?? "";
262
+ if (/^\.env(\..+)?$/.test(name) || name === ".npmrc") {
263
+ add(context, {
264
+ rule: "secret-file",
265
+ area: "security",
266
+ severity: "high",
267
+ file: file.path,
268
+ line: null,
269
+ message: `${file.path} usually holds secrets: it must not stay in the widget folder.`,
270
+ fix: "Move it out of the widget folder, and keep secrets out of the widget: anyone can read a widget's package.",
271
+ });
272
+ }
273
+ if (file.text === null)
274
+ continue;
275
+ // Long lines (minified code) are read in overlapping windows: the
276
+ // patterns then always run on bounded text.
277
+ const lines = file.text
278
+ .split("\n")
279
+ .map((text) => text.length <= SCAN_WINDOW
280
+ ? [text]
281
+ : Array.from({ length: Math.ceil(text.length / (SCAN_WINDOW - 256)) }, (_, i) => text.slice(i * (SCAN_WINDOW - 256), i * (SCAN_WINDOW - 256) + SCAN_WINDOW)));
282
+ for (const { name: what, pattern } of SECRET_PATTERNS) {
283
+ const index = lines.findIndex((windows) => windows.some((text) => pattern.test(text)));
284
+ if (index < 0)
285
+ continue;
286
+ add(context, {
287
+ rule: "secret",
288
+ area: "security",
289
+ severity: "high",
290
+ file: file.path,
291
+ line: index + 1,
292
+ message: `${file.path} seems to hold ${what}.`,
293
+ fix: "Remove it, and revoke it at its provider: anyone can read a widget's package, and reviewers read its sources. A widget cannot keep a secret; use an API that needs none, or one the user's own account authorizes through OverCrow.",
294
+ });
295
+ }
296
+ }
297
+ }
298
+ function identity(context) {
299
+ const id = String(context.project.manifest?.id ?? "");
300
+ if (isReservedId(id)) {
301
+ add(context, {
302
+ rule: "reserved-id",
303
+ area: "security",
304
+ severity: "high",
305
+ file: "manifest.json",
306
+ line: null,
307
+ message: `The ID ${id} is reserved for widgets published by PlayerVox.`,
308
+ fix: `Use ${ID_FORMS}. The ID is final: it can never be changed or reused.`,
309
+ });
310
+ }
311
+ else if (isPlaceholderId(id)) {
312
+ add(context, {
313
+ rule: "example-id",
314
+ area: "security",
315
+ severity: "medium",
316
+ file: "manifest.json",
317
+ line: null,
318
+ message: `The ID ${id} is a placeholder copied from an example.`,
319
+ fix: `Before you submit, choose ${ID_FORMS}. The ID is final: keep it for every version.`,
320
+ });
321
+ }
322
+ const files = new Set(context.project.files.map((file) => file.path));
323
+ if (!files.has("LICENSE")) {
324
+ add(context, {
325
+ rule: "license-missing",
326
+ area: "security",
327
+ severity: "medium",
328
+ file: null,
329
+ line: null,
330
+ message: "The widget has no LICENSE file.",
331
+ fix: "Add a LICENSE file with the text of your licence: any licence is accepted, including all rights reserved (create_widget writes the MIT licence).",
332
+ });
333
+ }
334
+ }
335
+ // ── Lightness ────────────────────────────────────────────────────────────
336
+ function timerValue(context, args) {
337
+ const first = args[0];
338
+ return first ? evaluate(first, context.constants) : undefined;
339
+ }
340
+ function fastTimers(context) {
341
+ const usesNetwork = calls(context.tokens, ["http", "fetch"]).length > 0;
342
+ for (const open of calls(context.tokens, ["timers", "every"])) {
343
+ const value = timerValue(context, callArguments(context.tokens, open));
344
+ const line = context.tokens[open]?.line ?? null;
345
+ if (value === undefined)
346
+ continue;
347
+ if (value < 1000) {
348
+ add(context, {
349
+ rule: "fast-timer",
350
+ area: "lightness",
351
+ severity: value < 250 ? "medium" : "low",
352
+ file: context.logicFile,
353
+ line,
354
+ message: `A timer runs the logic every ${value} ms (${Math.round(1000 / Math.max(value, 1))} times a second).`,
355
+ fix: 'Let the host do the ticking: <elapsed> counts time without any logic, timers.atEach("second") follows the clock, and style animations run without the logic. Keep a fast timer only while something really moves.',
356
+ example: { widget: "stopwatch", file: "view.ocml", find: "<elapsed" },
357
+ });
358
+ }
359
+ if (usesNetwork && value < 30_000) {
360
+ add(context, {
361
+ rule: "fast-polling",
362
+ area: "lightness",
363
+ severity: "medium",
364
+ file: context.logicFile,
365
+ line,
366
+ message: `A timer repeats every ${value} ms in a widget that uses the network: if it requests the network, it polls a server ${Math.round(60_000 / value)} times a minute.`,
367
+ fix: "Refresh every 30 s or more, back off after a failure (×2 up to ×8), and only while the data is shown.",
368
+ example: {
369
+ widget: "warframe-market",
370
+ file: "logic.ts",
371
+ find: "export function refreshInterval(",
372
+ },
373
+ });
374
+ }
375
+ }
376
+ }
377
+ /** The `as` of an `http.fetch` call: "json", "text", "bytes", "image" or undefined. */
378
+ function fetchAs(tokens, open) {
379
+ const options = callArguments(tokens, open)[1] ?? [];
380
+ for (let index = 0; index + 2 < options.length; index += 1) {
381
+ if (options[index]?.value === "as" &&
382
+ options[index + 1]?.value === ":" &&
383
+ options[index + 2]?.kind === "string") {
384
+ return options[index + 2]?.value;
385
+ }
386
+ }
387
+ return undefined;
388
+ }
389
+ function largeResponses(context) {
390
+ const rules = permissionsOf(context.project).network ?? [];
391
+ if (!Array.isArray(rules))
392
+ return;
393
+ const threshold = 256 * KIB;
394
+ rules.forEach((rule, index) => {
395
+ const bound = typeof rule.maxResponseBytes === "number"
396
+ ? rule.maxResponseBytes
397
+ : limit("MAX_HTTP_RESPONSE_BYTES");
398
+ if (bound <= threshold)
399
+ return;
400
+ const prefix = staticPrefix(String(rule.path ?? "")).replace(/\/$/, "");
401
+ for (const literal of literalIndexes(context, prefix)) {
402
+ // The fetch that requests this route: in the same call, or in the
403
+ // function that the literal is passed to (one level).
404
+ const owner = enclosing(context.ranges, literal);
405
+ const scope = owner ? context.tokens.slice(owner.start, owner.end) : context.tokens;
406
+ let as = calls(scope, ["http", "fetch"])
407
+ .map((open) => fetchAs(scope, open))
408
+ .find((value) => value !== undefined);
409
+ if (as === undefined) {
410
+ // `getText("/v2/items")`: look into the called function.
411
+ for (let back = literal - 1; back >= Math.max(0, literal - 3); back -= 1) {
412
+ const token = context.tokens[back];
413
+ if (token?.value === "(" && context.tokens[back - 1]?.kind === "identifier") {
414
+ const callee = context.ranges.find((range) => range.name === context.tokens[back - 1]?.value);
415
+ if (callee) {
416
+ const body = context.tokens.slice(callee.start, callee.end);
417
+ as = calls(body, ["http", "fetch"])
418
+ .map((open) => fetchAs(body, open))
419
+ .find((value) => value !== undefined);
420
+ }
421
+ break;
422
+ }
423
+ }
424
+ }
425
+ if (as === "json") {
426
+ add(context, {
427
+ rule: "large-response-one-turn",
428
+ area: "lightness",
429
+ severity: "medium",
430
+ file: context.logicFile,
431
+ line: context.tokens[literal]?.line ?? null,
432
+ message: `permissions.network[${index}] allows responses up to ${Math.round(bound / KIB)} KiB, and the logic receives it as JSON: the whole body is parsed in one turn of the logic (${limit("VM_TURN_BUDGET_MS")} ms budget) and held in memory at once.`,
433
+ fix: 'Lower maxResponseBytes if the real responses are small. For large ones, ask for as: "text" and parse it slice by slice over several turns (timers.after between slices), keeping only the fields you show.',
434
+ example: {
435
+ widget: "warframe-market",
436
+ file: "logic.ts",
437
+ find: "export class CatalogScanner",
438
+ },
439
+ });
440
+ return;
441
+ }
442
+ }
443
+ });
444
+ }
445
+ /** Arrays that live across turns: `state.x` and module-level `let/const x = []`. */
446
+ function unboundedLists(context) {
447
+ const { tokens } = context;
448
+ const moduleArrays = new Set();
449
+ let depth = 0;
450
+ tokens.forEach((token, index) => {
451
+ if (token.value === "{")
452
+ depth += 1;
453
+ if (token.value === "}")
454
+ depth -= 1;
455
+ if (depth !== 0 || (token.value !== "let" && token.value !== "const"))
456
+ return;
457
+ // `const name = [` or `const name: Type[] = [`.
458
+ for (let j = index + 2; j < Math.min(tokens.length - 1, index + 14); j += 1) {
459
+ if (tokens[j]?.value === ";")
460
+ return;
461
+ if (tokens[j]?.value === "=") {
462
+ if (tokens[j + 1]?.value === "[")
463
+ moduleArrays.add(tokens[index + 1]?.value ?? "");
464
+ return;
465
+ }
466
+ }
467
+ });
468
+ const reported = new Set();
469
+ tokens.forEach((token, index) => {
470
+ if (token.value !== "push" ||
471
+ tokens[index - 1]?.value !== "." ||
472
+ tokens[index + 1]?.value !== "(")
473
+ return;
474
+ // The pushed array's name: `state.items.push`, `items.push`.
475
+ const name = tokens[index - 2]?.value ?? "";
476
+ const isState = tokens[index - 3]?.value === "." && tokens[index - 4]?.value === "state";
477
+ if (!isState && !(moduleArrays.has(name) && tokens[index - 3]?.value !== "."))
478
+ return;
479
+ if (reported.has(name))
480
+ return;
481
+ const bounded = tokens.some((other, j) => other.value === name &&
482
+ tokens[j + 1]?.value === "." &&
483
+ ["length", "slice", "splice", "shift", "pop", "filter"].includes(tokens[j + 2]?.value ?? ""));
484
+ if (bounded)
485
+ return;
486
+ reported.add(name);
487
+ add(context, {
488
+ rule: "unbounded-list",
489
+ area: "lightness",
490
+ severity: "medium",
491
+ file: context.logicFile,
492
+ line: token.line,
493
+ message: `${isState ? "state." : ""}${name} grows with push and is never trimmed: it can grow for as long as the game runs.`,
494
+ fix: `Cap it: drop the oldest entries past a maximum (for example with slice), and show at most what fits. The view has a budget of ${limit("MAX_SCENE_NODES")} nodes.`,
495
+ example: { widget: "twitch-chat", file: "logic.ts", find: "export const HISTORY_MAX" },
496
+ });
497
+ });
498
+ }
499
+ function redraws(context) {
500
+ for (const open of calls(context.tokens, ["timers", "every"])) {
501
+ const close = callArguments(context.tokens, open);
502
+ const body = close.flat();
503
+ if (body.some((token, index) => token.value === "draw" && body[index + 1]?.value === "(")) {
504
+ add(context, {
505
+ rule: "redraw-on-timer",
506
+ area: "lightness",
507
+ severity: "low",
508
+ file: context.logicFile,
509
+ line: context.tokens[open]?.line ?? null,
510
+ message: "A timer redraws a canvas each time it fires.",
511
+ fix: "Redraw only when what the canvas shows has changed: compare with the last drawn value first.",
512
+ example: { widget: "playervox-rating", file: "logic.ts", find: 'draw("badge"' },
513
+ });
514
+ return;
515
+ }
516
+ }
517
+ }
518
+ function images(context) {
519
+ const preview = typeof context.project.listing?.preview === "string"
520
+ ? context.project.listing.preview
521
+ : null;
522
+ const maxEdge = limit("MAX_IMAGE_EDGE_PX");
523
+ for (const file of context.project.files) {
524
+ if (!file.path.startsWith("assets/") || !/\.(png|jpe?g|webp)$/i.test(file.path))
525
+ continue;
526
+ const isPreview = file.path === preview;
527
+ const bound = isPreview ? 256 * KIB : 64 * KIB;
528
+ if (file.bytes > bound) {
529
+ add(context, {
530
+ rule: "heavy-image",
531
+ area: "lightness",
532
+ severity: isPreview ? "high" : "medium",
533
+ file: file.path,
534
+ line: null,
535
+ message: `${file.path} weighs ${Math.round(file.bytes / KIB)} KiB${isPreview ? ": a preview over 256 KiB is refused at admission" : ""}.`,
536
+ fix: isPreview
537
+ ? 'Render the preview at 150 % in a scenario ("scale": 1500), in a 4:3 shape, and save it as an optimized PNG.'
538
+ : "Save the image at the size it is shown (logical pixels × the largest scale you support) and optimize it; prefer an icon or a style for simple shapes.",
539
+ example: { widget: "clock", file: "listing.json", find: '"preview"' },
540
+ });
541
+ }
542
+ if (file.image && (file.image.width > maxEdge || file.image.height > maxEdge)) {
543
+ add(context, {
544
+ rule: "image-too-large",
545
+ area: "lightness",
546
+ severity: "high",
547
+ file: file.path,
548
+ line: null,
549
+ message: `${file.path} is ${file.image.width}×${file.image.height} px: OverCrow refuses images over ${maxEdge} px on a side.`,
550
+ fix: "Resize it to the size it is shown.",
551
+ });
552
+ }
553
+ }
554
+ }
555
+ function dependencies(context) {
556
+ const manifest = context.project.packageJson;
557
+ if (!manifest)
558
+ return;
559
+ const extra = [];
560
+ for (const field of [
561
+ "dependencies",
562
+ "devDependencies",
563
+ "optionalDependencies",
564
+ "peerDependencies",
565
+ ]) {
566
+ const deps = manifest[field];
567
+ if (typeof deps !== "object" || deps === null)
568
+ continue;
569
+ for (const name of Object.keys(deps))
570
+ if (name !== "@overcrow/sdk" && name !== "typescript")
571
+ extra.push(name);
572
+ }
573
+ if (extra.length > 0) {
574
+ add(context, {
575
+ rule: "extra-dependencies",
576
+ area: "lightness",
577
+ severity: "medium",
578
+ file: "package.json",
579
+ line: null,
580
+ message: `package.json declares ${extra.slice(0, 6).join(", ")}${extra.length > 6 ? "…" : ""}: a widget's logic can import only @overcrow/sdk.`,
581
+ fix: "Remove them. Use the SDK and the host's elements, or copy the few lines you need (with their license).",
582
+ });
583
+ }
584
+ }
585
+ function heapAndLogic(context) {
586
+ const vm = context.project.manifest?.vm;
587
+ if (typeof vm?.heapMiB === "number" && vm.heapMiB > 16) {
588
+ add(context, {
589
+ rule: "larger-heap",
590
+ area: "lightness",
591
+ severity: "low",
592
+ file: "manifest.json",
593
+ line: null,
594
+ message: `The manifest asks for a ${vm.heapMiB} MiB heap instead of the default 16 MiB.`,
595
+ fix: "Ask for more only after the widget stops with resource_limit under real data; first bound what it keeps in memory.",
596
+ });
597
+ }
598
+ for (const token of context.tokens) {
599
+ if ((token.kind === "string" || token.kind === "template") && token.value.length > 8 * KIB) {
600
+ add(context, {
601
+ rule: "embedded-data",
602
+ area: "lightness",
603
+ severity: "low",
604
+ file: context.logicFile,
605
+ line: token.line,
606
+ message: `The logic embeds a text of ${Math.round(token.value.length / KIB)} KiB: it is held in memory for the widget's whole life.`,
607
+ fix: "Keep only what the widget shows; fetch or compute the rest when needed.",
608
+ });
609
+ return;
610
+ }
611
+ }
612
+ }
613
+ function animations(context) {
614
+ for (const [name, line] of infiniteAnimations(context.project.style)) {
615
+ const used = context.view.unconditionalClasses.get(name);
616
+ if (used !== undefined) {
617
+ add(context, {
618
+ rule: "endless-animation",
619
+ area: "lightness",
620
+ severity: "low",
621
+ file: "view.ocml",
622
+ line: used,
623
+ message: `The class ${name} runs an endless animation (style.ocss line ${line}) on an element that is always shown.`,
624
+ fix: "Apply an endless animation only while something is in progress (inside an <if>, or with a class computed from the state).",
625
+ example: { widget: "notes", file: "view.ocml", find: "selector-icon spin" },
626
+ });
627
+ }
628
+ }
629
+ }
630
+ /** Runs every rule on a read widget. */
631
+ export function runRules(project) {
632
+ const tokens = project.tokens;
633
+ const context = {
634
+ project,
635
+ tokens,
636
+ ranges: functions(tokens),
637
+ constants: numericConstants(tokens),
638
+ strings: tokens
639
+ .filter((token) => token.kind === "string" || token.kind === "template")
640
+ .map((token) => token.value),
641
+ view: viewFacts(project.view),
642
+ findings: [],
643
+ logicFile: project.logicPath ?? "logic.ts",
644
+ };
645
+ identity(context);
646
+ secrets(context);
647
+ if (project.manifest) {
648
+ unusedAuthority(context);
649
+ networkRules(context);
650
+ largeResponses(context);
651
+ }
652
+ clipboardOutsideGesture(context);
653
+ fastTimers(context);
654
+ unboundedLists(context);
655
+ redraws(context);
656
+ images(context);
657
+ dependencies(context);
658
+ heapAndLogic(context);
659
+ animations(context);
660
+ return context.findings;
661
+ }