@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,229 @@
1
+ // Downloads one pinned file over HTTPS. Only the expected hosts are
2
+ // contacted; redirects are followed by hand and bounded; the size must be
3
+ // exactly the pinned size; the SHA-256 is computed while reading, and the
4
+ // caller compares it with the pin before anything reads the file.
5
+ import { createHash } from "node:crypto";
6
+ import { createWriteStream } from "node:fs";
7
+ import { unlink } from "node:fs/promises";
8
+ import { request as httpRequest } from "node:http";
9
+ import { request as httpsRequest } from "node:https";
10
+ import { connect as tlsConnect } from "node:tls";
11
+ export class DownloadError extends Error {
12
+ reason;
13
+ constructor(reason) {
14
+ super(reason);
15
+ this.reason = reason;
16
+ this.name = "DownloadError";
17
+ }
18
+ }
19
+ const REDIRECTS = new Set([301, 302, 303, 307, 308]);
20
+ /** Checks that `target` is an HTTPS URL of an allowed host, without credentials. */
21
+ export function checkUrl(target, allowedHosts, testPort) {
22
+ if (target.protocol !== "https:")
23
+ throw new DownloadError(`refused a non-HTTPS address (${target.protocol})`);
24
+ if (target.username || target.password)
25
+ throw new DownloadError("refused an address with credentials");
26
+ if (!allowedHosts.includes(target.hostname.toLowerCase())) {
27
+ throw new DownloadError(`refused a redirect to another host (${target.hostname})`);
28
+ }
29
+ const port = target.port === "" ? 443 : Number(target.port);
30
+ if (port !== 443 && port !== testPort)
31
+ throw new DownloadError(`refused port ${port}`);
32
+ }
33
+ /**
34
+ * Downloads `url` into `destination` (created, never overwritten) and
35
+ * returns its size and SHA-256. On any failure the partial file is removed.
36
+ */
37
+ export async function downloadPinned(url, destination, options) {
38
+ const maxRedirects = options.maxRedirects ?? 3;
39
+ const deadline = Date.now() + (options.totalTimeoutMs ?? 10 * 60_000);
40
+ let target = new URL(url);
41
+ checkUrl(target, options.allowedHosts, options.port);
42
+ try {
43
+ for (let hop = 0;; hop += 1) {
44
+ const response = await get(target, options, deadline);
45
+ const status = response.statusCode ?? 0;
46
+ if (REDIRECTS.has(status)) {
47
+ response.resume();
48
+ const location = response.headers.location;
49
+ if (!location)
50
+ throw new DownloadError(`redirect ${status} without a location`);
51
+ if (hop >= maxRedirects)
52
+ throw new DownloadError("too many redirects");
53
+ target = new URL(location, target);
54
+ checkUrl(target, options.allowedHosts, options.port);
55
+ continue;
56
+ }
57
+ if (status !== 200) {
58
+ response.resume();
59
+ throw new DownloadError(status === 404 ? "the file is not published (404)" : `the server answered ${status}`);
60
+ }
61
+ return await save(response, destination, options, deadline);
62
+ }
63
+ }
64
+ catch (error) {
65
+ await unlink(destination).catch(() => undefined);
66
+ if (error instanceof DownloadError)
67
+ throw error;
68
+ throw new DownloadError(networkReason(error));
69
+ }
70
+ }
71
+ function networkReason(error) {
72
+ const code = error.code;
73
+ if (code === "ENOTFOUND" || code === "EAI_AGAIN")
74
+ return "the address could not be resolved (offline?)";
75
+ if (code === "ECONNREFUSED")
76
+ return "the connection was refused";
77
+ if (code === "ECONNRESET" || code === "EPIPE")
78
+ return "the connection was cut";
79
+ if (code?.startsWith("ERR_TLS") || code?.includes("CERT"))
80
+ return `the TLS certificate was refused (${code})`;
81
+ if (code === "ABORT_ERR")
82
+ return "cancelled";
83
+ return code ? `network error (${code})` : "network error";
84
+ }
85
+ /** One GET request, directly or through the proxy's CONNECT tunnel. */
86
+ async function get(target, options, deadline) {
87
+ const host = target.hostname;
88
+ const port = target.port === "" ? 443 : Number(target.port);
89
+ const tunnel = options.proxy
90
+ ? await connectThroughProxy(options.proxy, host, port, options, deadline)
91
+ : undefined;
92
+ const requestOptions = {
93
+ method: "GET",
94
+ host,
95
+ port,
96
+ path: `${target.pathname}${target.search}`,
97
+ servername: host,
98
+ headers: {
99
+ "user-agent": "overcrow-mcp",
100
+ accept: "application/octet-stream",
101
+ // The size check needs the bytes as stored.
102
+ "accept-encoding": "identity",
103
+ },
104
+ rejectUnauthorized: true,
105
+ agent: false,
106
+ ...(options.ca !== undefined ? { ca: options.ca } : {}),
107
+ ...(tunnel ? { createConnection: () => tunnel } : {}),
108
+ };
109
+ return new Promise((resolve, reject) => {
110
+ const request = httpsRequest(requestOptions, resolve);
111
+ guard(request, options, deadline, reject);
112
+ request.end();
113
+ });
114
+ }
115
+ /** Applies the connection, idle and total timeouts and the abort signal to `request`. */
116
+ function guard(request, options, deadline, reject) {
117
+ const connectTimer = setTimeout(() => request.destroy(new DownloadError("the connection timed out")), Math.min(options.connectTimeoutMs ?? 30_000, Math.max(0, deadline - Date.now())));
118
+ request.once("response", () => clearTimeout(connectTimer));
119
+ request.once("close", () => clearTimeout(connectTimer));
120
+ request.setTimeout(options.idleTimeoutMs ?? 60_000, () => request.destroy(new DownloadError("the server stopped sending")));
121
+ const onAbort = () => request.destroy(new DownloadError("cancelled"));
122
+ options.signal?.addEventListener("abort", onAbort, { once: true });
123
+ request.once("close", () => options.signal?.removeEventListener("abort", onAbort));
124
+ request.once("error", reject);
125
+ }
126
+ /** Opens a CONNECT tunnel through an `http://` proxy and returns its TLS socket. */
127
+ async function connectThroughProxy(proxy, host, port, options, deadline) {
128
+ let proxyUrl;
129
+ try {
130
+ proxyUrl = new URL(proxy.includes("://") ? proxy : `http://${proxy}`);
131
+ }
132
+ catch {
133
+ throw new DownloadError("the proxy setting (HTTPS_PROXY) is not a valid address");
134
+ }
135
+ if (proxyUrl.protocol !== "http:") {
136
+ throw new DownloadError("only an http:// proxy is supported (HTTPS_PROXY)");
137
+ }
138
+ const headers = { host: `${host}:${port}` };
139
+ if (proxyUrl.username) {
140
+ const credentials = `${decodeURIComponent(proxyUrl.username)}:${decodeURIComponent(proxyUrl.password)}`;
141
+ headers["proxy-authorization"] = `Basic ${Buffer.from(credentials).toString("base64")}`;
142
+ }
143
+ const socket = await new Promise((resolve, reject) => {
144
+ const request = httpRequest({
145
+ method: "CONNECT",
146
+ host: proxyUrl.hostname,
147
+ port: proxyUrl.port === "" ? 80 : Number(proxyUrl.port),
148
+ path: `${host}:${port}`,
149
+ headers,
150
+ agent: false,
151
+ });
152
+ guard(request, options, deadline, reject);
153
+ request.once("connect", (response, tunnelSocket) => {
154
+ if (response.statusCode !== 200) {
155
+ tunnelSocket.destroy();
156
+ reject(new DownloadError(`the proxy refused the connection (${response.statusCode})`));
157
+ return;
158
+ }
159
+ resolve(tunnelSocket);
160
+ });
161
+ request.end();
162
+ });
163
+ return tlsConnect({
164
+ socket,
165
+ servername: host,
166
+ rejectUnauthorized: true,
167
+ ...(options.ca !== undefined ? { ca: options.ca } : {}),
168
+ });
169
+ }
170
+ /** Streams the body to `destination`, bounded by the expected size. */
171
+ async function save(response, destination, options, deadline) {
172
+ const declared = response.headers["content-length"];
173
+ if (declared !== undefined && Number(declared) !== options.expectedSize) {
174
+ response.resume();
175
+ throw new DownloadError(`the file has another size than the pinned one (${declared} bytes)`);
176
+ }
177
+ const encoding = response.headers["content-encoding"];
178
+ if (encoding !== undefined && encoding !== "identity") {
179
+ response.resume();
180
+ throw new DownloadError(`unexpected content encoding (${encoding})`);
181
+ }
182
+ const hash = createHash("sha256");
183
+ const output = createWriteStream(destination, { flags: "wx", mode: 0o600 });
184
+ let received = 0;
185
+ let lastReport = 0;
186
+ await new Promise((resolve, reject) => {
187
+ let settled = false;
188
+ const fail = (error) => {
189
+ // Destroying the response emits its own events: the first reason wins.
190
+ if (settled)
191
+ return;
192
+ settled = true;
193
+ reject(error);
194
+ response.destroy();
195
+ output.destroy();
196
+ };
197
+ const totalTimer = setTimeout(() => fail(new DownloadError("the download took too long")), Math.max(0, deadline - Date.now()));
198
+ output.once("error", (error) => fail(error));
199
+ response.once("error", (error) => fail(error));
200
+ response.once("aborted", () => fail(new DownloadError("the connection was cut")));
201
+ response.on("data", (chunk) => {
202
+ received += chunk.length;
203
+ if (received > options.expectedSize) {
204
+ fail(new DownloadError("the file is larger than the pinned size"));
205
+ return;
206
+ }
207
+ hash.update(chunk);
208
+ if (!output.write(chunk)) {
209
+ response.pause();
210
+ output.once("drain", () => response.resume());
211
+ }
212
+ if (options.onProgress && received - lastReport >= 1 << 20) {
213
+ lastReport = received;
214
+ options.onProgress(received, options.expectedSize);
215
+ }
216
+ });
217
+ response.once("end", () => {
218
+ clearTimeout(totalTimer);
219
+ if (received !== options.expectedSize) {
220
+ fail(new DownloadError("the download ended early"));
221
+ return;
222
+ }
223
+ settled = true;
224
+ output.end(() => resolve());
225
+ });
226
+ output.once("close", () => clearTimeout(totalTimer));
227
+ });
228
+ return { sha256: hash.digest("hex"), size: received };
229
+ }
@@ -0,0 +1,280 @@
1
+ // Installs the creator tools of one platform from a ZIP whose SHA-256 is the
2
+ // pinned one: every file is extracted by the strict reader and checked
3
+ // against the pin, the ZIP's own SHA256SUMS, cli.json and runtimes.json must
4
+ // agree, and the CLI must pin exactly the runtime beside it. The files land
5
+ // in a private temporary directory that is renamed into place at the end;
6
+ // on any failure nothing is kept. Nothing goes on the PATH.
7
+ import { createHash, randomBytes } from "node:crypto";
8
+ import { lstat, mkdir, mkdtemp, open, readdir, readFile, rename, rm } from "node:fs/promises";
9
+ import { join } from "node:path";
10
+ import { extractEntry, readEntries } from "./zip.js";
11
+ export class InstallError extends Error {
12
+ constructor(message) {
13
+ super(message);
14
+ this.name = "InstallError";
15
+ }
16
+ }
17
+ const ZIP_LIMITS = { maxEntries: 64, maxEntryBytes: 256 << 20, maxTotalBytes: 512 << 20 };
18
+ const MAX_METADATA_BYTES = 64 * 1024;
19
+ export function sha256(data) {
20
+ return createHash("sha256").update(data).digest("hex");
21
+ }
22
+ function executableSuffix(platform) {
23
+ return platform === "windows-x86_64" ? ".exe" : "";
24
+ }
25
+ /** The file names of a creator tools ZIP, derived from the pin's file list. */
26
+ export function toolNames(release, platform, names) {
27
+ const licenses = names
28
+ .map((name) => /^overcrow-widget-(\d+\.\d+\.\d+(?:-[a-z]+\.\d+)?)-LICENSE\.txt$/.exec(name)?.[1])
29
+ .filter((version) => version !== undefined);
30
+ if (licenses.length !== 1)
31
+ throw new InstallError("the creator tools do not hold exactly one CLI");
32
+ const cliVersion = licenses[0];
33
+ const suffix = executableSuffix(platform);
34
+ const expected = {
35
+ cli: `overcrow-widget-${cliVersion}-${platform}${suffix}`,
36
+ cliLicense: `overcrow-widget-${cliVersion}-LICENSE.txt`,
37
+ cliNotices: `overcrow-widget-${cliVersion}-THIRD-PARTY-NOTICES.md`,
38
+ runtime: `overcrow-widget-headless-${release}-${platform}${suffix}`,
39
+ runtimeLicense: `overcrow-widget-headless-${release}-LICENSE.md`,
40
+ };
41
+ const all = new Set([
42
+ ...Object.values(expected),
43
+ "cli.json",
44
+ "runtimes.json",
45
+ "README.txt",
46
+ "SHA256SUMS",
47
+ ]);
48
+ if (all.size !== names.length || !names.every((name) => all.has(name))) {
49
+ throw new InstallError("the creator tools have missing or unexpected files");
50
+ }
51
+ return { cliVersion, ...expected };
52
+ }
53
+ /**
54
+ * Checks `archive` against `pinned` and returns its files by name. The
55
+ * SHA-256 of the whole archive is checked before any byte is parsed.
56
+ */
57
+ export function verifyArchive(archive, pinned) {
58
+ if (archive.length !== pinned.size || sha256(archive) !== pinned.sha256) {
59
+ throw new InstallError("the creator tools ZIP is not the pinned one (size or SHA-256 differs)");
60
+ }
61
+ const root = pinned.name.replace(/\.zip$/, "");
62
+ const entries = readEntries(archive, ZIP_LIMITS);
63
+ const expected = new Map(pinned.files.map((file) => [file.path, file]));
64
+ if (entries.length !== expected.size)
65
+ throw new InstallError("the creator tools ZIP has another file list than the pin");
66
+ const files = new Map();
67
+ for (const entry of entries) {
68
+ const pin = expected.get(entry.name);
69
+ if (!pin || entry.size !== pin.size)
70
+ throw new InstallError(`unexpected file in the creator tools ZIP: ${entry.name}`);
71
+ const data = extractEntry(archive, entry);
72
+ if (sha256(data) !== pin.sha256)
73
+ throw new InstallError(`a file of the creator tools ZIP differs from the pin: ${entry.name}`);
74
+ files.set(entry.name.slice(root.length + 1), data);
75
+ }
76
+ return files;
77
+ }
78
+ function parseJson(data, name) {
79
+ if (!data || data.length > MAX_METADATA_BYTES)
80
+ throw new InstallError(`${name} is missing or too large`);
81
+ try {
82
+ const value = JSON.parse(data.toString("utf8"));
83
+ if (typeof value === "object" && value !== null && !Array.isArray(value))
84
+ return value;
85
+ }
86
+ catch {
87
+ // Reported below.
88
+ }
89
+ throw new InstallError(`${name} is not a JSON object`);
90
+ }
91
+ /**
92
+ * The checks between the files of the ZIP: SHA256SUMS lists every other
93
+ * file with its digest, runtimes.json describes the runtime, and cli.json
94
+ * describes the CLI and pins that same runtime.
95
+ */
96
+ export function crossCheck(files, release, platform) {
97
+ const names = toolNames(release, platform, [...files.keys()]);
98
+ const sums = files.get("SHA256SUMS")?.toString("utf8") ?? "";
99
+ const listed = new Map();
100
+ for (const line of sums.split("\n")) {
101
+ if (line === "")
102
+ continue;
103
+ const match = /^([0-9a-f]{64}) [ *]([A-Za-z0-9][A-Za-z0-9._+-]*)$/.exec(line);
104
+ if (!match || listed.has(match[2]))
105
+ throw new InstallError("SHA256SUMS is malformed");
106
+ listed.set(match[2], match[1]);
107
+ }
108
+ for (const [name, data] of files) {
109
+ if (name === "SHA256SUMS")
110
+ continue;
111
+ if (listed.get(name) !== sha256(data))
112
+ throw new InstallError(`SHA256SUMS does not match ${name}`);
113
+ }
114
+ if (listed.size !== files.size - 1)
115
+ throw new InstallError("SHA256SUMS lists files that are not in the ZIP");
116
+ const cliData = files.get(names.cli);
117
+ const runtimeData = files.get(names.runtime);
118
+ const runtimeSha = sha256(runtimeData);
119
+ const runtimes = parseJson(files.get("runtimes.json"), "runtimes.json");
120
+ const runtimeEntry = runtimes.runtimes?.[`headless-${platform}`];
121
+ if (runtimes.version !== release ||
122
+ runtimeEntry?.name !== names.runtime ||
123
+ runtimeEntry?.sha256 !== runtimeSha ||
124
+ runtimeEntry?.size !== runtimeData.length) {
125
+ throw new InstallError("runtimes.json does not describe the runtime of the ZIP");
126
+ }
127
+ const cli = parseJson(files.get("cli.json"), "cli.json");
128
+ const runtimePin = cli.runtime;
129
+ const cliFiles = Array.isArray(cli.files) ? cli.files : [];
130
+ const cliEntry = cliFiles.find((file) => file.name === names.cli);
131
+ if (cli.version !== names.cliVersion ||
132
+ runtimePin?.version !== release ||
133
+ runtimePin.sha256?.[platform] !== runtimeSha ||
134
+ cliEntry?.sha256 !== sha256(cliData) ||
135
+ cliEntry?.size !== cliData.length) {
136
+ throw new InstallError("cli.json does not describe this CLI, or pins another runtime");
137
+ }
138
+ return { ...names, runtimeSha256: runtimeSha };
139
+ }
140
+ /** Creates the cache root (0700) and refuses a link in its place. */
141
+ export async function prepareCacheRoot(cacheRoot) {
142
+ await mkdir(cacheRoot, { recursive: true, mode: 0o700 });
143
+ const info = await lstat(cacheRoot);
144
+ if (!info.isDirectory())
145
+ throw new InstallError("the tools cache is not a plain directory");
146
+ if (process.platform !== "win32" &&
147
+ typeof process.getuid === "function" &&
148
+ info.uid !== process.getuid()) {
149
+ throw new InstallError("the tools cache belongs to another user");
150
+ }
151
+ }
152
+ async function writeDurably(path, data, mode) {
153
+ const handle = await open(path, "wx", mode);
154
+ try {
155
+ await handle.writeFile(data);
156
+ await handle.chmod(mode);
157
+ await handle.sync();
158
+ }
159
+ finally {
160
+ await handle.close();
161
+ }
162
+ }
163
+ async function syncDirectory(path) {
164
+ if (process.platform === "win32")
165
+ return;
166
+ const handle = await open(path, "r");
167
+ try {
168
+ await handle.sync();
169
+ }
170
+ finally {
171
+ await handle.close();
172
+ }
173
+ }
174
+ export function installDirectory(cacheRoot, release, platform) {
175
+ return join(cacheRoot, `${release}-${platform}`);
176
+ }
177
+ /**
178
+ * Installs `archive` (already read in memory) into the cache. Returns the
179
+ * installed tools; if another server installed the same release meanwhile,
180
+ * its copy is checked and used.
181
+ */
182
+ export async function install(archive, pinned, release, platform, cacheRoot, probe) {
183
+ const files = verifyArchive(archive, pinned);
184
+ const names = crossCheck(files, release, platform);
185
+ await prepareCacheRoot(cacheRoot);
186
+ const working = await mkdtemp(join(cacheRoot, ".install-"));
187
+ try {
188
+ for (const [name, data] of files) {
189
+ const executable = name === names.cli || name === names.runtime;
190
+ await writeDurably(join(working, name), data, executable ? 0o755 : 0o644);
191
+ }
192
+ await syncDirectory(working);
193
+ await checkVersion(join(working, names.cli), working, release, platform, names, probe);
194
+ const final = installDirectory(cacheRoot, release, platform);
195
+ try {
196
+ await rename(working, final);
197
+ }
198
+ catch (error) {
199
+ const code = error.code;
200
+ if (code !== "ENOTEMPTY" && code !== "EEXIST" && code !== "EPERM")
201
+ throw error;
202
+ // Another server finished first: use its copy once checked.
203
+ const existing = await loadInstalled(pinned, release, platform, cacheRoot);
204
+ if (existing)
205
+ return existing;
206
+ // A damaged copy of our own cache: set it aside, then take its place.
207
+ const damaged = `${final}.damaged-${randomBytes(4).toString("hex")}`;
208
+ await rename(final, damaged);
209
+ await rm(damaged, { recursive: true, force: true });
210
+ await rename(working, final);
211
+ }
212
+ await syncDirectory(cacheRoot);
213
+ return {
214
+ release,
215
+ platform,
216
+ directory: final,
217
+ cli: join(final, names.cli),
218
+ runtime: join(final, names.runtime),
219
+ cliVersion: names.cliVersion,
220
+ runtimeSha256: names.runtimeSha256,
221
+ };
222
+ }
223
+ finally {
224
+ await rm(working, { recursive: true, force: true });
225
+ }
226
+ }
227
+ async function checkVersion(cli, cwd, release, platform, names, probe) {
228
+ const report = (await probe(cli, cwd));
229
+ if (report?.version !== names.cliVersion ||
230
+ report.apiVersion !== 1 ||
231
+ report.runtime?.version !== release ||
232
+ report.runtime.sha256?.[platform] !== names.runtimeSha256) {
233
+ throw new InstallError("the CLI does not report the version and runtime of its ZIP");
234
+ }
235
+ }
236
+ /**
237
+ * The installed tools of the pinned release, after checking every file
238
+ * against the pin again; undefined when they are absent or damaged.
239
+ */
240
+ export async function loadInstalled(pinned, release, platform, cacheRoot) {
241
+ const directory = installDirectory(cacheRoot, release, platform);
242
+ try {
243
+ const info = await lstat(directory);
244
+ if (!info.isDirectory())
245
+ return undefined;
246
+ const root = pinned.name.replace(/\.zip$/, "");
247
+ const expected = new Map(pinned.files.map((file) => [file.path.slice(root.length + 1), file]));
248
+ const present = await readdir(directory);
249
+ if (present.length !== expected.size)
250
+ return undefined;
251
+ for (const name of present) {
252
+ const pin = expected.get(name);
253
+ if (!pin)
254
+ return undefined;
255
+ const path = join(directory, name);
256
+ const fileInfo = await lstat(path);
257
+ if (!fileInfo.isFile() || fileInfo.size !== pin.size)
258
+ return undefined;
259
+ if (sha256(await readFile(path)) !== pin.sha256)
260
+ return undefined;
261
+ }
262
+ const names = toolNames(release, platform, present);
263
+ return {
264
+ release,
265
+ platform,
266
+ directory,
267
+ cli: join(directory, names.cli),
268
+ runtime: join(directory, names.runtime),
269
+ cliVersion: names.cliVersion,
270
+ runtimeSha256: expected.get(names.runtime).sha256,
271
+ };
272
+ }
273
+ catch {
274
+ return undefined;
275
+ }
276
+ }
277
+ /** A fresh, private path for a download in the cache root. */
278
+ export function downloadPath(cacheRoot) {
279
+ return join(cacheRoot, `.download-${randomBytes(8).toString("hex")}.zip`);
280
+ }
@@ -0,0 +1,85 @@
1
+ // The creator tools release this version of the package uses: its version
2
+ // and, for each platform, the ZIP's name, size and SHA-256 and every file
3
+ // in it (the `creatorTools` entry of the release's public release.json,
4
+ // copied by `scripts/pin-release.mjs`). Nothing else is ever downloaded.
5
+ import { readFileSync } from "node:fs";
6
+ export const RELEASES_REPOSITORY = "Valhallab/playervox-overcrow-releases";
7
+ export const DOWNLOAD_HOSTS = [
8
+ "github.com",
9
+ "release-assets.githubusercontent.com",
10
+ "objects.githubusercontent.com",
11
+ ];
12
+ const HEX64 = /^[0-9a-f]{64}$/;
13
+ const INTEGRITY = /^sha512-[A-Za-z0-9+/]{86}==$/;
14
+ const VERSION = /^\d+\.\d+\.\d+(?:-[a-z]+\.\d+)?$/;
15
+ const FILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
16
+ /** The address of a pinned ZIP. */
17
+ export function toolsUrl(release, name, base = "https://github.com") {
18
+ return `${base}/${RELEASES_REPOSITORY}/releases/download/v${release}/${name}`;
19
+ }
20
+ /** Checks the shape of a pin; throws on anything unexpected. */
21
+ export function validatePin(value) {
22
+ const fail = (why) => {
23
+ throw new Error(`invalid pin: ${why}`);
24
+ };
25
+ if (typeof value !== "object" || value === null)
26
+ fail("not an object");
27
+ const { release, tools, npm } = value;
28
+ const packages = (npm ?? {});
29
+ const pinnedPackage = (name) => {
30
+ const { version, integrity } = (packages[name] ?? {});
31
+ if (typeof version !== "string" || !VERSION.test(version))
32
+ fail(`${name} version`);
33
+ if (typeof integrity !== "string" || !INTEGRITY.test(integrity))
34
+ fail(`${name} integrity`);
35
+ return { version: version, integrity: integrity };
36
+ };
37
+ const npmPins = { sdk: pinnedPackage("@overcrow/sdk"), typescript: pinnedPackage("typescript") };
38
+ if (release === null)
39
+ return { release: null, tools: {}, npm: npmPins };
40
+ if (typeof release !== "string" || !VERSION.test(release))
41
+ fail("release version");
42
+ if (typeof tools !== "object" || tools === null)
43
+ fail("tools");
44
+ const result = { release: release, tools: {}, npm: npmPins };
45
+ for (const platform of ["linux-x86_64", "windows-x86_64"]) {
46
+ const entry = tools[platform];
47
+ if (typeof entry !== "object" || entry === null)
48
+ fail(`missing ${platform}`);
49
+ const { name, size, sha256, files } = entry;
50
+ const root = `overcrow-creator-tools-${release}-${platform}`;
51
+ if (name !== `${root}.zip`)
52
+ fail(`${platform} name`);
53
+ if (!Number.isSafeInteger(size) || size <= 0)
54
+ fail(`${platform} size`);
55
+ if (typeof sha256 !== "string" || !HEX64.test(sha256))
56
+ fail(`${platform} digest`);
57
+ if (!Array.isArray(files) || files.length === 0 || files.length > 64)
58
+ fail(`${platform} files`);
59
+ const checked = [];
60
+ for (const file of files) {
61
+ const { path, size: fileSize, sha256: fileSha } = (file ?? {});
62
+ if (typeof path !== "string" ||
63
+ !path.startsWith(`${root}/`) ||
64
+ !FILE_NAME.test(path.slice(root.length + 1))) {
65
+ fail(`${platform} file path`);
66
+ }
67
+ if (!Number.isSafeInteger(fileSize) || fileSize <= 0)
68
+ fail(`${platform} file size`);
69
+ if (typeof fileSha !== "string" || !HEX64.test(fileSha))
70
+ fail(`${platform} file digest`);
71
+ checked.push({ path: path, size: fileSize, sha256: fileSha });
72
+ }
73
+ result.tools[platform] = {
74
+ name: name,
75
+ size: size,
76
+ sha256: sha256,
77
+ files: checked,
78
+ };
79
+ }
80
+ return result;
81
+ }
82
+ /** The pin shipped with the package (`dist/pin.json`). */
83
+ export function shippedPin() {
84
+ return validatePin(JSON.parse(readFileSync(new URL("./pin.json", import.meta.url), "utf8")));
85
+ }