@vfrey/vfrey 0.0.1 → 1.0.0-beta.0

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 (603) hide show
  1. package/LICENSE +33 -0
  2. package/README.md +78 -3
  3. package/accounts.example.json +61 -0
  4. package/accounts.first-run.json +41 -0
  5. package/bin/vfrey.mjs +36 -0
  6. package/integration-tools/README.md +43 -0
  7. package/integration-tools/image/pillow.md +32 -0
  8. package/integration-tools/image/playwright.md +33 -0
  9. package/integration-tools/image/resvg.md +36 -0
  10. package/integration-tools/image/satori.md +36 -0
  11. package/integration-tools/image/sharp.md +38 -0
  12. package/integration-tools/video/blender.md +34 -0
  13. package/integration-tools/video/ffmpeg.md +41 -0
  14. package/integration-tools/video/manim.md +37 -0
  15. package/integration-tools/video/motion-canvas.md +37 -0
  16. package/integration-tools/video/remotion.md +50 -0
  17. package/integration-tools/video/revideo.md +32 -0
  18. package/package.json +74 -6
  19. package/skills/vfrey-orchestrator/SKILL.md +15 -0
  20. package/skills/vfrey-orchestrator/channel-video/SKILL.md +41 -0
  21. package/skills/vfrey-product/SKILL.md +63 -0
  22. package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/cook-one.svg +14 -0
  23. package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/cook-two.svg +22 -0
  24. package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/index.html +70 -0
  25. package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/cook-one.svg +14 -0
  26. package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/cook-two.svg +22 -0
  27. package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/index.tsx +49 -0
  28. package/skills/vfrey-product/examples/concurrency-vs-parallelism/script.json +15 -0
  29. package/skills/vfrey-product/examples/octopus-facts/index.html +60 -0
  30. package/skills/vfrey-product/examples/octopus-facts/octopus.svg +8 -0
  31. package/skills/vfrey-product/examples/octopus-facts/script.json +14 -0
  32. package/skills/vfrey-product/push/SKILL.md +15 -0
  33. package/skills/vfrey-product/scenes/NOTICE +5 -0
  34. package/skills/vfrey-product/scenes/blocks.md +57 -0
  35. package/skills/vfrey-product/scenes/craft.md +118 -0
  36. package/skills/vfrey-product/scenes/motion.md +88 -0
  37. package/src/analytics/dashboard.ts +109 -0
  38. package/src/analytics/index.ts +260 -0
  39. package/src/api/client.ts +282 -0
  40. package/src/api/commands.ts +330 -0
  41. package/src/api/config.ts +34 -0
  42. package/src/api/creators.ts +463 -0
  43. package/src/api/credentials.ts +152 -0
  44. package/src/api/mail.ts +333 -0
  45. package/src/api/topics.ts +207 -0
  46. package/src/api/types.ts +205 -0
  47. package/src/cli.ts +467 -0
  48. package/src/cloud/executor.ts +281 -0
  49. package/src/cloud/job.ts +106 -0
  50. package/src/cloud/render-job.ts +187 -0
  51. package/src/config.ts +309 -0
  52. package/src/dashboard-link.ts +180 -0
  53. package/src/dashboard-start.ts +103 -0
  54. package/src/desk/channel.ts +64 -0
  55. package/src/desk/cli.ts +216 -0
  56. package/src/desk/creators-route.ts +129 -0
  57. package/src/desk/fonts/JetBrainsMono-OFL.txt +93 -0
  58. package/src/desk/fonts/google-sans-flex-latin.woff2 +0 -0
  59. package/src/desk/fonts/jetbrains-mono-medium.woff2 +0 -0
  60. package/src/desk/http.ts +460 -0
  61. package/src/desk/mail-route.ts +103 -0
  62. package/src/desk/overlay.ts +112 -0
  63. package/src/desk/preview.ts +422 -0
  64. package/src/desk/screens.ts +149 -0
  65. package/src/desk/secrets.ts +143 -0
  66. package/src/desk/series.ts +127 -0
  67. package/src/desk/state.ts +77 -0
  68. package/src/desk/studio-route.ts +119 -0
  69. package/src/desk/tick-runner.ts +66 -0
  70. package/src/desk/tick.ts +56 -0
  71. package/src/desk/topics-route.ts +41 -0
  72. package/src/desk/ui/app.css +505 -0
  73. package/src/desk/ui/app.js +185 -0
  74. package/src/desk/ui/apple-icon.png +0 -0
  75. package/src/desk/ui/components.js +79 -0
  76. package/src/desk/ui/core.js +156 -0
  77. package/src/desk/ui/favicon.ico +0 -0
  78. package/src/desk/ui/icon.svg +1 -0
  79. package/src/desk/ui/icons.js +49 -0
  80. package/src/desk/ui/index.html +41 -0
  81. package/src/desk/ui/metrics-math.d.ts +22 -0
  82. package/src/desk/ui/metrics-math.js +135 -0
  83. package/src/desk/ui/theme.js +5 -0
  84. package/src/desk/ui/views-creators.js +209 -0
  85. package/src/desk/ui/views-keys.js +114 -0
  86. package/src/desk/ui/views-mail.js +188 -0
  87. package/src/desk/ui/views-misc.js +123 -0
  88. package/src/desk/ui/views-review.js +119 -0
  89. package/src/desk/ui/views-runs.js +350 -0
  90. package/src/desk/ui/views-screens.js +163 -0
  91. package/src/desk/ui/views-series.js +318 -0
  92. package/src/desk/ui/views-topics.js +87 -0
  93. package/src/desk/verbs.ts +361 -0
  94. package/src/doctor.ts +212 -0
  95. package/src/drain.ts +14 -0
  96. package/src/engage/index.ts +217 -0
  97. package/src/generate/index.ts +110 -0
  98. package/src/generate/script.ts +152 -0
  99. package/src/generate/title.ts +78 -0
  100. package/src/help.ts +233 -0
  101. package/src/home.ts +45 -0
  102. package/src/llm/client.ts +177 -0
  103. package/src/log.ts +7 -0
  104. package/src/oauth/tokens.ts +189 -0
  105. package/src/oauth/vault.ts +72 -0
  106. package/src/pipeline/review.ts +642 -0
  107. package/src/pipeline/run.ts +443 -0
  108. package/src/pipeline/series-guard.ts +26 -0
  109. package/src/pipeline/source-fetch.ts +69 -0
  110. package/src/pipeline/stages.ts +728 -0
  111. package/src/pipeline/topics-source.ts +90 -0
  112. package/src/product/commands.ts +296 -0
  113. package/src/product/config-file.ts +38 -0
  114. package/src/product/engine-choice.ts +89 -0
  115. package/src/product/engines.ts +103 -0
  116. package/src/product/insights.ts +50 -0
  117. package/src/product/keys.ts +67 -0
  118. package/src/product/paths.ts +18 -0
  119. package/src/product/probes.ts +88 -0
  120. package/src/product/push-flow.ts +90 -0
  121. package/src/product/render-video.ts +21 -0
  122. package/src/product/short.ts +62 -0
  123. package/src/product/update.ts +32 -0
  124. package/src/project/canonical.ts +14 -0
  125. package/src/project/commands.ts +177 -0
  126. package/src/project/dashboard-status.ts +28 -0
  127. package/src/project/discover.ts +19 -0
  128. package/src/project/index-file.ts +73 -0
  129. package/src/project/marker.ts +87 -0
  130. package/src/project/migrate.ts +201 -0
  131. package/src/project/names.ts +27 -0
  132. package/src/project/notice.ts +16 -0
  133. package/src/project/open.ts +88 -0
  134. package/src/project/scaffold.ts +112 -0
  135. package/src/project/templates.ts +42 -0
  136. package/src/project/videos.ts +64 -0
  137. package/src/publish/facebook.ts +91 -0
  138. package/src/publish/index.ts +12 -0
  139. package/src/publish/instagram.ts +97 -0
  140. package/src/publish/rate.ts +62 -0
  141. package/src/publish/session.ts +242 -0
  142. package/src/publish/tiktok.ts +202 -0
  143. package/src/publish/youtube.ts +177 -0
  144. package/src/queue/desk-ops.ts +1170 -0
  145. package/src/queue/seal.ts +3 -0
  146. package/src/queue/store.ts +1006 -0
  147. package/src/remotion/AiLabel.tsx +26 -0
  148. package/src/remotion/Root.tsx +73 -0
  149. package/src/remotion/defaults.ts +26 -0
  150. package/src/remotion/films/claude/ClaudeFilm.tsx +1014 -0
  151. package/src/remotion/films/engine/EngineMark.tsx +17 -0
  152. package/src/remotion/films/rea/ReaShort.tsx +552 -0
  153. package/src/remotion/films/rea/drawings.tsx +907 -0
  154. package/src/remotion/films/rea/timing.ts +144 -0
  155. package/src/remotion/index.ts +4 -0
  156. package/src/remotion/templates/EditorialSplit.tsx +39 -0
  157. package/src/remotion/templates/KineticType.tsx +39 -0
  158. package/src/remotion/templates/SignalBoard.tsx +50 -0
  159. package/src/remotion/timing.ts +22 -0
  160. package/src/render/chrome.ts +78 -0
  161. package/src/render/engine-mark.ts +29 -0
  162. package/src/render/framey-template.ts +130 -0
  163. package/src/render/html-engine.ts +149 -0
  164. package/src/render/node-crypto-stub.ts +11 -0
  165. package/src/render/probe.ts +29 -0
  166. package/src/render/props.ts +17 -0
  167. package/src/render/vertical-short.ts +35 -0
  168. package/src/render/webpack.ts +33 -0
  169. package/src/render/worker.ts +65 -0
  170. package/src/scene/assets/fonts/TitanOne-OFL.txt +94 -0
  171. package/src/scene/assets/fonts/titan-one.woff2 +0 -0
  172. package/src/scene/assets/gsap/LICENSE.txt +8 -0
  173. package/src/scene/assets/gsap/gsap.min.js +11 -0
  174. package/src/scene/audio.ts +54 -0
  175. package/src/scene/blocks/LICENSE +190 -0
  176. package/src/scene/blocks/NOTICE +5 -0
  177. package/src/scene/blocks/bar-chart-race/bar-chart-race.html +585 -0
  178. package/src/scene/blocks/beat-freeze-cut/beat-freeze-cut.html +874 -0
  179. package/src/scene/blocks/camcorder-hud/camcorder-hud.html +164 -0
  180. package/src/scene/blocks/camera-dolly-zoom/camera-dolly-zoom.html +355 -0
  181. package/src/scene/blocks/code-diff/code-diff.html +817 -0
  182. package/src/scene/blocks/code-snippet-apple-terminal-pro/code-snippet-apple-terminal-pro.html +248 -0
  183. package/src/scene/blocks/code-typing/code-typing.html +764 -0
  184. package/src/scene/blocks/data-chart/data-chart.html +452 -0
  185. package/src/scene/blocks/flowchart-vertical/flowchart-vertical.html +454 -0
  186. package/src/scene/blocks/hw-scribble-transition/hw-scribble-transition.html +153 -0
  187. package/src/scene/blocks/index.json +824 -0
  188. package/src/scene/blocks/lt-accent-underline/lt-accent-underline.html +107 -0
  189. package/src/scene/blocks/lt-bold-block/lt-bold-block.html +109 -0
  190. package/src/scene/blocks/lt-clean-bar/lt-clean-bar.html +112 -0
  191. package/src/scene/blocks/lt-dark-card/lt-dark-card.html +111 -0
  192. package/src/scene/blocks/lt-kicker-name/lt-kicker-name.html +114 -0
  193. package/src/scene/blocks/lt-mask-reveal/lt-mask-reveal.html +120 -0
  194. package/src/scene/blocks/lt-neon-border/lt-neon-border.html +391 -0
  195. package/src/scene/blocks/lt-stack-bars/lt-stack-bars.html +109 -0
  196. package/src/scene/blocks/macos-notification/macos-notification.html +194 -0
  197. package/src/scene/blocks/mk-background/mk-background.html +236 -0
  198. package/src/scene/blocks/mk-callout-highlight/mk-callout-highlight.html +183 -0
  199. package/src/scene/blocks/mk-clone-wall-transition/mk-clone-wall-transition.html +226 -0
  200. package/src/scene/blocks/mk-line-graph/mk-line-graph.html +293 -0
  201. package/src/scene/blocks/mk-progress-stat/mk-progress-stat.html +186 -0
  202. package/src/scene/blocks/mk-specs-list/mk-specs-list.html +223 -0
  203. package/src/scene/blocks/news-ticker/news-ticker.html +409 -0
  204. package/src/scene/blocks/organic-light-leak-overlay/organic-light-leak-overlay.html +126 -0
  205. package/src/scene/blocks/oscilloscope-trace/oscilloscope-trace.html +519 -0
  206. package/src/scene/blocks/snow/snow.html +205 -0
  207. package/src/scene/blocks/split-flap-board/split-flap-board.html +347 -0
  208. package/src/scene/blocks/spotify-card/spotify-card.html +294 -0
  209. package/src/scene/blocks/starfield/starfield.html +188 -0
  210. package/src/scene/blocks/thread-message-stack/thread-message-stack.html +239 -0
  211. package/src/scene/blocks/week-in-merges/week-in-merges.html +470 -0
  212. package/src/scene/blocks/x-post/x-post.html +437 -0
  213. package/src/scene/blocks/yt-comment-card/yt-comment-card.html +319 -0
  214. package/src/scene/blocks/yt-lcd-background/yt-lcd-background.html +307 -0
  215. package/src/scene/blocks/yt-prism-title/yt-prism-title.html +205 -0
  216. package/src/scene/blocks.ts +229 -0
  217. package/src/scene/command.ts +272 -0
  218. package/src/scene/figure.ts +181 -0
  219. package/src/scene/glyphs.ts +107 -0
  220. package/src/scene/init.ts +86 -0
  221. package/src/scene/inspect/css-tree.d.ts +6 -0
  222. package/src/scene/inspect/page-scripts.ts +272 -0
  223. package/src/scene/inspect/report.ts +405 -0
  224. package/src/scene/inspect/run.ts +200 -0
  225. package/src/scene/inspect/source.ts +236 -0
  226. package/src/scene/label.ts +131 -0
  227. package/src/scene/net-guard.ts +74 -0
  228. package/src/scene/render.ts +292 -0
  229. package/src/scene/runtime/browser.ts +199 -0
  230. package/src/scene/runtime/core.ts +181 -0
  231. package/src/scene/runtime/react.tsx +115 -0
  232. package/src/scene/runtime/transitions.ts +133 -0
  233. package/src/scene/sanitize.ts +181 -0
  234. package/src/scene/scene.ts +152 -0
  235. package/src/scene/studio-command.ts +48 -0
  236. package/src/scene/studio-skin.ts +59 -0
  237. package/src/scene/studio.ts +337 -0
  238. package/src/scene/supply.ts +111 -0
  239. package/src/scene/timing.ts +288 -0
  240. package/src/scene/transitions/LICENSE +190 -0
  241. package/src/scene/transitions/NOTICE +5 -0
  242. package/src/scene/transitions/SOURCE.json +24 -0
  243. package/src/scene/transitions/transitions.global.js +38 -0
  244. package/src/scene/transitions.ts +26 -0
  245. package/src/service/all-projects.ts +153 -0
  246. package/src/service/commands.ts +256 -0
  247. package/src/service/due.ts +82 -0
  248. package/src/service/lock.ts +101 -0
  249. package/src/service/log-file.ts +56 -0
  250. package/src/service/state.ts +41 -0
  251. package/src/service/units.ts +90 -0
  252. package/src/service/worker.ts +51 -0
  253. package/src/skill-agents.ts +74 -0
  254. package/src/skill-install.ts +334 -0
  255. package/src/sync-write.ts +21 -0
  256. package/src/tick-report.ts +205 -0
  257. package/src/tts.ts +124 -0
  258. package/vendor/@vfrey/contracts/dist/account.d.ts +97 -0
  259. package/vendor/@vfrey/contracts/dist/account.js +27 -0
  260. package/vendor/@vfrey/contracts/dist/checks.d.ts +8 -0
  261. package/vendor/@vfrey/contracts/dist/checks.js +1 -0
  262. package/vendor/@vfrey/contracts/dist/credentials/crypto.d.ts +4 -0
  263. package/vendor/@vfrey/contracts/dist/credentials/crypto.js +38 -0
  264. package/vendor/@vfrey/contracts/dist/domain/advice-persona.d.ts +10 -0
  265. package/vendor/@vfrey/contracts/dist/domain/advice-persona.js +24 -0
  266. package/vendor/@vfrey/contracts/dist/domain/backoff.d.ts +13 -0
  267. package/vendor/@vfrey/contracts/dist/domain/backoff.js +27 -0
  268. package/vendor/@vfrey/contracts/dist/domain/cadence.d.ts +29 -0
  269. package/vendor/@vfrey/contracts/dist/domain/cadence.js +95 -0
  270. package/vendor/@vfrey/contracts/dist/domain/creators.d.ts +146 -0
  271. package/vendor/@vfrey/contracts/dist/domain/creators.js +2 -0
  272. package/vendor/@vfrey/contracts/dist/domain/disclosure.d.ts +127 -0
  273. package/vendor/@vfrey/contracts/dist/domain/disclosure.js +250 -0
  274. package/vendor/@vfrey/contracts/dist/domain/errors.d.ts +34 -0
  275. package/vendor/@vfrey/contracts/dist/domain/errors.js +68 -0
  276. package/vendor/@vfrey/contracts/dist/domain/redact.d.ts +1 -0
  277. package/vendor/@vfrey/contracts/dist/domain/redact.js +21 -0
  278. package/vendor/@vfrey/contracts/dist/domain/revenue.d.ts +13 -0
  279. package/vendor/@vfrey/contracts/dist/domain/revenue.js +16 -0
  280. package/vendor/@vfrey/contracts/dist/domain/safety.d.ts +20 -0
  281. package/vendor/@vfrey/contracts/dist/domain/safety.js +61 -0
  282. package/vendor/@vfrey/contracts/dist/domain/similarity.d.ts +13 -0
  283. package/vendor/@vfrey/contracts/dist/domain/similarity.js +100 -0
  284. package/vendor/@vfrey/contracts/dist/domain/templates.d.ts +18 -0
  285. package/vendor/@vfrey/contracts/dist/domain/templates.js +67 -0
  286. package/vendor/@vfrey/contracts/dist/domain/tiktok-chunks.d.ts +19 -0
  287. package/vendor/@vfrey/contracts/dist/domain/tiktok-chunks.js +52 -0
  288. package/vendor/@vfrey/contracts/dist/domain/topics.d.ts +78 -0
  289. package/vendor/@vfrey/contracts/dist/domain/topics.js +4 -0
  290. package/vendor/@vfrey/contracts/dist/domain/types.d.ts +216 -0
  291. package/vendor/@vfrey/contracts/dist/domain/types.js +1 -0
  292. package/vendor/@vfrey/contracts/dist/domain/variation.d.ts +57 -0
  293. package/vendor/@vfrey/contracts/dist/domain/variation.js +278 -0
  294. package/vendor/@vfrey/contracts/dist/events.d.ts +20 -0
  295. package/vendor/@vfrey/contracts/dist/events.js +1 -0
  296. package/vendor/@vfrey/contracts/dist/http.d.ts +11 -0
  297. package/vendor/@vfrey/contracts/dist/http.js +37 -0
  298. package/vendor/@vfrey/contracts/dist/index.d.ts +22 -0
  299. package/vendor/@vfrey/contracts/dist/index.js +22 -0
  300. package/vendor/@vfrey/contracts/dist/log.d.ts +2 -0
  301. package/vendor/@vfrey/contracts/dist/log.js +10 -0
  302. package/vendor/@vfrey/contracts/dist/mail.d.ts +55 -0
  303. package/vendor/@vfrey/contracts/dist/mail.js +1 -0
  304. package/vendor/@vfrey/contracts/package.json +18 -0
  305. package/vendor/@vfrey/ingest/dist/index.d.ts +41 -0
  306. package/vendor/@vfrey/ingest/dist/index.js +221 -0
  307. package/vendor/@vfrey/ingest/dist/parse.d.ts +5 -0
  308. package/vendor/@vfrey/ingest/dist/parse.js +165 -0
  309. package/vendor/@vfrey/ingest/package.json +21 -0
  310. package/vendor/framey/LICENSE +190 -0
  311. package/vendor/framey/NOTICE +15 -0
  312. package/vendor/framey/SOURCE.json +24 -0
  313. package/vendor/framey/STUDIO-SOURCE.json +9 -0
  314. package/vendor/framey/bin/framey-localize-fonts.mjs +12 -0
  315. package/vendor/framey/bin/framey.mjs +16 -0
  316. package/vendor/framey/dist/add-VMJD5KBS.js +75 -0
  317. package/vendor/framey/dist/animatedGifPrep-QPKGJ6PM.js +43 -0
  318. package/vendor/framey/dist/api.d.ts +211 -0
  319. package/vendor/framey/dist/api.js +76 -0
  320. package/vendor/framey/dist/appHistory-WW56FZTT.js +31 -0
  321. package/vendor/framey/dist/asset-WQKGOELE.js +26 -0
  322. package/vendor/framey/dist/audio-RSJWVWRA.js +1936 -0
  323. package/vendor/framey/dist/batchRender-3HPICDCT.js +366 -0
  324. package/vendor/framey/dist/beats-HL2BDUZI.js +273 -0
  325. package/vendor/framey/dist/benchmark-RWCWHI5Z.js +230 -0
  326. package/vendor/framey/dist/browser-ECYET3Q2.js +206 -0
  327. package/vendor/framey/dist/bundleWithLocalizedFonts-ALYBQEQT.js +52 -0
  328. package/vendor/framey/dist/captureCompositionFrame-4DA6SEH2.js +94 -0
  329. package/vendor/framey/dist/catalog-5JVDEUWK.js +979 -0
  330. package/vendor/framey/dist/catch-up-NEBCL5NO.js +106 -0
  331. package/vendor/framey/dist/check-F5N544ML.js +111 -0
  332. package/vendor/framey/dist/checkBrowser-LZ66CMJG.js +110 -0
  333. package/vendor/framey/dist/chunk-2K7Z2HVP.js +372 -0
  334. package/vendor/framey/dist/chunk-2UZQHDM7.js +323 -0
  335. package/vendor/framey/dist/chunk-2X4PQEYM.js +34 -0
  336. package/vendor/framey/dist/chunk-3422KMTD.js +168 -0
  337. package/vendor/framey/dist/chunk-3FYD4YEK.js +72 -0
  338. package/vendor/framey/dist/chunk-3GXQPCJC.js +682 -0
  339. package/vendor/framey/dist/chunk-3LM6YPBR.js +179 -0
  340. package/vendor/framey/dist/chunk-442JNXRM.js +20 -0
  341. package/vendor/framey/dist/chunk-4BNN5FGX.js +182 -0
  342. package/vendor/framey/dist/chunk-4P7NX5BM.js +11532 -0
  343. package/vendor/framey/dist/chunk-4UMPIPCQ.js +88 -0
  344. package/vendor/framey/dist/chunk-4YYJOFKS.js +28 -0
  345. package/vendor/framey/dist/chunk-54HJ63Q2.js +29 -0
  346. package/vendor/framey/dist/chunk-5ETJX6WM.js +89 -0
  347. package/vendor/framey/dist/chunk-5SG54JRS.js +68 -0
  348. package/vendor/framey/dist/chunk-634HSJ3J.js +58 -0
  349. package/vendor/framey/dist/chunk-66LREXGS.js +48 -0
  350. package/vendor/framey/dist/chunk-6BEFEWFB.js +25 -0
  351. package/vendor/framey/dist/chunk-6BNTGNGD.js +286 -0
  352. package/vendor/framey/dist/chunk-6DDPOHMK.js +140 -0
  353. package/vendor/framey/dist/chunk-6GV5XSOR.js +214 -0
  354. package/vendor/framey/dist/chunk-6JR2XUEE.js +127 -0
  355. package/vendor/framey/dist/chunk-73QVATIM.js +362 -0
  356. package/vendor/framey/dist/chunk-7FMUTGOH.js +279 -0
  357. package/vendor/framey/dist/chunk-7JQFJ6JJ.js +291 -0
  358. package/vendor/framey/dist/chunk-AHPGAESX.js +113 -0
  359. package/vendor/framey/dist/chunk-AHX34EFP.js +15 -0
  360. package/vendor/framey/dist/chunk-AJ5CSAEZ.js +487 -0
  361. package/vendor/framey/dist/chunk-B7RJV74O.js +5047 -0
  362. package/vendor/framey/dist/chunk-BL4U6U52.js +254 -0
  363. package/vendor/framey/dist/chunk-BSQ7XANM.js +210 -0
  364. package/vendor/framey/dist/chunk-BZ56MTDX.js +6 -0
  365. package/vendor/framey/dist/chunk-C5BO7PMT.js +282 -0
  366. package/vendor/framey/dist/chunk-CFUCHO4T.js +358 -0
  367. package/vendor/framey/dist/chunk-CZGHFATT.js +31 -0
  368. package/vendor/framey/dist/chunk-DLA44CMS.js +5649 -0
  369. package/vendor/framey/dist/chunk-DPVXBVLL.js +31292 -0
  370. package/vendor/framey/dist/chunk-DQBY7FRR.js +397 -0
  371. package/vendor/framey/dist/chunk-EAMLBAHA.js +154 -0
  372. package/vendor/framey/dist/chunk-EOVYQ3IG.js +143 -0
  373. package/vendor/framey/dist/chunk-EVMY7IPL.js +612 -0
  374. package/vendor/framey/dist/chunk-FG4BBFZI.js +122 -0
  375. package/vendor/framey/dist/chunk-FI4I7CME.js +56 -0
  376. package/vendor/framey/dist/chunk-FLREUEHW.js +2081 -0
  377. package/vendor/framey/dist/chunk-FUFP7A7S.js +1126 -0
  378. package/vendor/framey/dist/chunk-FV3PIKGH.js +56 -0
  379. package/vendor/framey/dist/chunk-FXMKHHPO.js +123 -0
  380. package/vendor/framey/dist/chunk-GGZAM5VH.js +1187 -0
  381. package/vendor/framey/dist/chunk-GKZEVJIB.js +39 -0
  382. package/vendor/framey/dist/chunk-GMECXFAN.js +57 -0
  383. package/vendor/framey/dist/chunk-GNEFDLFB.js +621 -0
  384. package/vendor/framey/dist/chunk-GPMLSOEG.js +19274 -0
  385. package/vendor/framey/dist/chunk-GPYVT5EE.js +59 -0
  386. package/vendor/framey/dist/chunk-H6IEMZEV.js +40 -0
  387. package/vendor/framey/dist/chunk-HEFQCIXR.js +170 -0
  388. package/vendor/framey/dist/chunk-HFQ4CWHA.js +118 -0
  389. package/vendor/framey/dist/chunk-HPDTCOKE.js +304 -0
  390. package/vendor/framey/dist/chunk-HTUHQKDB.js +548 -0
  391. package/vendor/framey/dist/chunk-HU2SPFNG.js +103 -0
  392. package/vendor/framey/dist/chunk-HVW7ABXD.js +19 -0
  393. package/vendor/framey/dist/chunk-IG7NDKXO.js +26 -0
  394. package/vendor/framey/dist/chunk-II3J3WXK.js +246 -0
  395. package/vendor/framey/dist/chunk-IR35BCLK.js +13 -0
  396. package/vendor/framey/dist/chunk-ISN5GRYI.js +20 -0
  397. package/vendor/framey/dist/chunk-IUONB55J.js +669 -0
  398. package/vendor/framey/dist/chunk-IVSUQR2B.js +161 -0
  399. package/vendor/framey/dist/chunk-IXU6PQSW.js +259 -0
  400. package/vendor/framey/dist/chunk-IZCW2NKT.js +29 -0
  401. package/vendor/framey/dist/chunk-JGXJU5PF.js +68 -0
  402. package/vendor/framey/dist/chunk-JHOCK3DG.js +27 -0
  403. package/vendor/framey/dist/chunk-JVBRJMOP.js +121 -0
  404. package/vendor/framey/dist/chunk-K4MJX7SR.js +17 -0
  405. package/vendor/framey/dist/chunk-K4WKGXRC.js +18 -0
  406. package/vendor/framey/dist/chunk-KGPLIH7B.js +127 -0
  407. package/vendor/framey/dist/chunk-KLIKCX6Q.js +63 -0
  408. package/vendor/framey/dist/chunk-KXTKKBOC.js +61 -0
  409. package/vendor/framey/dist/chunk-KZI44OPA.js +185 -0
  410. package/vendor/framey/dist/chunk-LEKZNRKI.js +45 -0
  411. package/vendor/framey/dist/chunk-LFGJWOVA.js +61 -0
  412. package/vendor/framey/dist/chunk-LLMSX6V6.js +17 -0
  413. package/vendor/framey/dist/chunk-LTGTN2TH.js +195 -0
  414. package/vendor/framey/dist/chunk-M2WBOQ2Q.js +54 -0
  415. package/vendor/framey/dist/chunk-MI5FTLYK.js +399 -0
  416. package/vendor/framey/dist/chunk-MLQP2QUF.js +1809 -0
  417. package/vendor/framey/dist/chunk-MMQ52NIZ.js +956 -0
  418. package/vendor/framey/dist/chunk-ND46XKK3.js +742 -0
  419. package/vendor/framey/dist/chunk-NYEXNSHM.js +504 -0
  420. package/vendor/framey/dist/chunk-O4YRB27L.js +57 -0
  421. package/vendor/framey/dist/chunk-O7BGSQ5F.js +92 -0
  422. package/vendor/framey/dist/chunk-OFQWTKPW.js +117 -0
  423. package/vendor/framey/dist/chunk-P5ZBLXJ6.js +91 -0
  424. package/vendor/framey/dist/chunk-P6LJ4Y32.js +4662 -0
  425. package/vendor/framey/dist/chunk-PKZ7RK5N.js +281 -0
  426. package/vendor/framey/dist/chunk-PURHAXFL.js +15 -0
  427. package/vendor/framey/dist/chunk-Q5X2B5WT.js +29 -0
  428. package/vendor/framey/dist/chunk-QAMKOG67.js +248 -0
  429. package/vendor/framey/dist/chunk-R6YBK4XX.js +514 -0
  430. package/vendor/framey/dist/chunk-RGLQURIX.js +2997 -0
  431. package/vendor/framey/dist/chunk-RNV3DM4S.js +3573 -0
  432. package/vendor/framey/dist/chunk-ROEW726S.js +4638 -0
  433. package/vendor/framey/dist/chunk-RQVDTPGD.js +6 -0
  434. package/vendor/framey/dist/chunk-RVZHGOGR.js +26 -0
  435. package/vendor/framey/dist/chunk-RXATE6FZ.js +385 -0
  436. package/vendor/framey/dist/chunk-S3S4TGEH.js +424 -0
  437. package/vendor/framey/dist/chunk-SDYDNHEE.js +11202 -0
  438. package/vendor/framey/dist/chunk-SSWBU67I.js +173 -0
  439. package/vendor/framey/dist/chunk-ST35C4K6.js +28 -0
  440. package/vendor/framey/dist/chunk-SZNTYTIV.js +990 -0
  441. package/vendor/framey/dist/chunk-T2EUWCJE.js +29 -0
  442. package/vendor/framey/dist/chunk-T6AGUTZJ.js +444 -0
  443. package/vendor/framey/dist/chunk-THJALHFJ.js +11131 -0
  444. package/vendor/framey/dist/chunk-TIQB63MX.js +18128 -0
  445. package/vendor/framey/dist/chunk-U242RTCT.js +134 -0
  446. package/vendor/framey/dist/chunk-UJZREAV6.js +3901 -0
  447. package/vendor/framey/dist/chunk-UMD3V7CU.js +264 -0
  448. package/vendor/framey/dist/chunk-UOFAE5RF.js +42 -0
  449. package/vendor/framey/dist/chunk-UTEIUGAZ.js +29 -0
  450. package/vendor/framey/dist/chunk-VAS6HQX3.js +138 -0
  451. package/vendor/framey/dist/chunk-VC76R6W2.js +27 -0
  452. package/vendor/framey/dist/chunk-VFZ7W5W7.js +226 -0
  453. package/vendor/framey/dist/chunk-VHNLZAE2.js +348 -0
  454. package/vendor/framey/dist/chunk-VMC4WSH5.js +278 -0
  455. package/vendor/framey/dist/chunk-VORQIH64.js +108 -0
  456. package/vendor/framey/dist/chunk-VTXFRIHO.js +186 -0
  457. package/vendor/framey/dist/chunk-W2LGJSEN.js +182 -0
  458. package/vendor/framey/dist/chunk-WKBM7NDG.js +32 -0
  459. package/vendor/framey/dist/chunk-WMQNIQDJ.js +2057 -0
  460. package/vendor/framey/dist/chunk-WN4XZC5F.js +504 -0
  461. package/vendor/framey/dist/chunk-WQ6ZXHFQ.js +133 -0
  462. package/vendor/framey/dist/chunk-X7HBSTCZ.js +53 -0
  463. package/vendor/framey/dist/chunk-X7MTZ4T2.js +6 -0
  464. package/vendor/framey/dist/chunk-XPTOJZJF.js +308 -0
  465. package/vendor/framey/dist/chunk-XT6MXC7A.js +6 -0
  466. package/vendor/framey/dist/chunk-XT7X6QCP.js +6 -0
  467. package/vendor/framey/dist/chunk-XTMDWNEC.js +2820 -0
  468. package/vendor/framey/dist/chunk-XUHOIDKG.js +6 -0
  469. package/vendor/framey/dist/chunk-XX7LTW5Y.js +138 -0
  470. package/vendor/framey/dist/chunk-Y3YGTZEG.js +382 -0
  471. package/vendor/framey/dist/chunk-Y5SBSGN7.js +346 -0
  472. package/vendor/framey/dist/chunk-YCMVI4MZ.js +625 -0
  473. package/vendor/framey/dist/chunk-YOR4CTCE.js +261 -0
  474. package/vendor/framey/dist/chunk-YPRBGITP.js +2748 -0
  475. package/vendor/framey/dist/chunk-ZKDMAOFU.js +32 -0
  476. package/vendor/framey/dist/chunk-ZW6DURPF.js +1318 -0
  477. package/vendor/framey/dist/clean-ZJOGDKDM.js +336 -0
  478. package/vendor/framey/dist/cli.js +439 -0
  479. package/vendor/framey/dist/cloudrun-HGJ3B6ZV.js +767 -0
  480. package/vendor/framey/dist/commands/contrast-audit.browser.js +643 -0
  481. package/vendor/framey/dist/commands/layout-audit.browser.js +2914 -0
  482. package/vendor/framey/dist/commands/motion-sample.browser.js +74 -0
  483. package/vendor/framey/dist/commands/motion-signature.browser.js +475 -0
  484. package/vendor/framey/dist/compare-W7UY3BAZ.js +463 -0
  485. package/vendor/framey/dist/compiler-TM3W6GYF.js +136 -0
  486. package/vendor/framey/dist/component-QF5VGYMU.js +176 -0
  487. package/vendor/framey/dist/compositions-XT3PMNDF.js +238 -0
  488. package/vendor/framey/dist/contactSheet-7W6GYHBQ.js +24 -0
  489. package/vendor/framey/dist/deploy-6FJPWKF2.js +299 -0
  490. package/vendor/framey/dist/destroy-HE2RIPKZ.js +57 -0
  491. package/vendor/framey/dist/deterministicFonts-RADD3BSX.js +50 -0
  492. package/vendor/framey/dist/dist-4S2FGGVY.js +422 -0
  493. package/vendor/framey/dist/dist-BE4B2EZE.js +32 -0
  494. package/vendor/framey/dist/dist-EWXJGN63.js +129 -0
  495. package/vendor/framey/dist/docs-VKIRFEEX.js +131 -0
  496. package/vendor/framey/dist/doctor-2UNJ2G7U.js +74 -0
  497. package/vendor/framey/dist/dom-37K6YOH4.js +14 -0
  498. package/vendor/framey/dist/dotEnv-7PZQTVIM.js +54 -0
  499. package/vendor/framey/dist/events-ACY3P2PA.js +76 -0
  500. package/vendor/framey/dist/events-IHB3B2MK.js +69 -0
  501. package/vendor/framey/dist/ffmpeg-DR5LGGSK.js +63 -0
  502. package/vendor/framey/dist/figma-BODVUPHB.js +63 -0
  503. package/vendor/framey/dist/fontData.generated-NXAOI4GH.js +13 -0
  504. package/vendor/framey/dist/fontLocalizeCli.js +142 -0
  505. package/vendor/framey/dist/gpuPolicy-XLEXORZY.js +49 -0
  506. package/vendor/framey/dist/grade-compare-GAXFQ74C.js +661 -0
  507. package/vendor/framey/dist/gsapParser-IEUSLYDJ.js +2885 -0
  508. package/vendor/framey/dist/gsapParserAcorn-LZJKCFQI.js +30 -0
  509. package/vendor/framey/dist/help-VCB5X2HS.js +184 -0
  510. package/vendor/framey/dist/history-ENGZEVFT.js +471 -0
  511. package/vendor/framey/dist/hyperframe.manifest.json +22 -0
  512. package/vendor/framey/dist/hyperframe.runtime.iife.js +1132 -0
  513. package/vendor/framey/dist/hyperframes-player.global.js +478 -0
  514. package/vendor/framey/dist/hyperframes-slideshow.global.js +148 -0
  515. package/vendor/framey/dist/info-WPCJHBMU.js +182 -0
  516. package/vendor/framey/dist/init-G4WGLYGD.js +1176 -0
  517. package/vendor/framey/dist/inspect-5IQ6CNEQ.js +106 -0
  518. package/vendor/framey/dist/installer-ML4HWUV7.js +40 -0
  519. package/vendor/framey/dist/keyframes-Q6XJO7VW.js +2755 -0
  520. package/vendor/framey/dist/lambda-A7JC74JF.js +468 -0
  521. package/vendor/framey/dist/layout-YET6XNXA.js +95 -0
  522. package/vendor/framey/dist/lint-I7FZN2VJ.js +162 -0
  523. package/vendor/framey/dist/lintProject-PDB3UN72.js +30 -0
  524. package/vendor/framey/dist/manager-CD6LJ7ZN.js +45 -0
  525. package/vendor/framey/dist/manager-WA4F2GST.js +67 -0
  526. package/vendor/framey/dist/media-treatment-BXYS4WVA.js +1030 -0
  527. package/vendor/framey/dist/media-use-LWH3MANK.js +113 -0
  528. package/vendor/framey/dist/models-K363FYM4.js +195 -0
  529. package/vendor/framey/dist/motionShot-GPAVWXLH.js +640 -0
  530. package/vendor/framey/dist/normalize-FASIPBUM.js +24 -0
  531. package/vendor/framey/dist/normalize-audio-VDWYRHN6.js +268 -0
  532. package/vendor/framey/dist/open-E3TB6P6J.js +128 -0
  533. package/vendor/framey/dist/parakeetRunner-UP34UOI2.js +58 -0
  534. package/vendor/framey/dist/pipeline-VXIUYEBU.js +64 -0
  535. package/vendor/framey/dist/play-BUTDQ4DV.js +353 -0
  536. package/vendor/framey/dist/policies-OLDYGFK5.js +309 -0
  537. package/vendor/framey/dist/preflight-2KZ2GWAZ.js +55 -0
  538. package/vendor/framey/dist/present-GXYSM7NX.js +353 -0
  539. package/vendor/framey/dist/preview-EKZ33JOJ.js +1910 -0
  540. package/vendor/framey/dist/producer-SKU5EV3J.js +13 -0
  541. package/vendor/framey/dist/progress-ZWB4Z4ET.js +76 -0
  542. package/vendor/framey/dist/projectConfig-3MZAYVTS.js +60 -0
  543. package/vendor/framey/dist/publication-DTKWNF74.js +33 -0
  544. package/vendor/framey/dist/remove-background-OY5L7XN2.js +265 -0
  545. package/vendor/framey/dist/render-CDV4GDFX.js +187 -0
  546. package/vendor/framey/dist/render-YE5YAMOD.js +2620 -0
  547. package/vendor/framey/dist/render-batch-ZAMFPCLL.js +274 -0
  548. package/vendor/framey/dist/renderSetupWorker.js +120 -0
  549. package/vendor/framey/dist/resolver-D5BIPSEW.js +42 -0
  550. package/vendor/framey/dist/runtimeVersion.js +18 -0
  551. package/vendor/framey/dist/sam-WFZXI4F7.js +23 -0
  552. package/vendor/framey/dist/shaderTransitionWorker.js +44 -0
  553. package/vendor/framey/dist/sherpa-RQWLV7I3.js +79 -0
  554. package/vendor/framey/dist/sherpaWorker.js +84 -0
  555. package/vendor/framey/dist/sites-AMOVABTK.js +54 -0
  556. package/vendor/framey/dist/skill-ZHVLH7KX.js +15 -0
  557. package/vendor/framey/dist/slideshow-SJGK4RQS.js +24 -0
  558. package/vendor/framey/dist/snapshot-TUXL6LBQ.js +1515 -0
  559. package/vendor/framey/dist/src-JHQGB2S6.js +215 -0
  560. package/vendor/framey/dist/src-Z5PGEMXB.js +532 -0
  561. package/vendor/framey/dist/state-B6H3WHCF.js +28 -0
  562. package/vendor/framey/dist/staticProjectServer-DFVZOUGI.js +63 -0
  563. package/vendor/framey/dist/studio/assets/hyperframes-player-DLaKjw0L.js +478 -0
  564. package/vendor/framey/dist/studio/assets/index-BX3KHhGX.js +71 -0
  565. package/vendor/framey/dist/studio/assets/index-CYbvOVp-.css +1 -0
  566. package/vendor/framey/dist/studio/assets/index-Dp3QEUGL.js +402 -0
  567. package/vendor/framey/dist/studio/assets/index-qrTAXbqc.js +531 -0
  568. package/vendor/framey/dist/studio/favicon.svg +14 -0
  569. package/vendor/framey/dist/studio/icons/timeline/audio.svg +7 -0
  570. package/vendor/framey/dist/studio/icons/timeline/captions.svg +5 -0
  571. package/vendor/framey/dist/studio/icons/timeline/composition.svg +12 -0
  572. package/vendor/framey/dist/studio/icons/timeline/image.svg +18 -0
  573. package/vendor/framey/dist/studio/icons/timeline/music.svg +10 -0
  574. package/vendor/framey/dist/studio/icons/timeline/text.svg +3 -0
  575. package/vendor/framey/dist/studio/index.html +23 -0
  576. package/vendor/framey/dist/studio-api-2NECGRMB.js +179 -0
  577. package/vendor/framey/dist/studioSelectionClient-IUUPKUHZ.js +25 -0
  578. package/vendor/framey/dist/studioServer-N2OPUMOQ.js +1569 -0
  579. package/vendor/framey/dist/synthesize-PKTB5ATG.js +183 -0
  580. package/vendor/framey/dist/system-7F3UEHQT.js +27 -0
  581. package/vendor/framey/dist/telemetry-DQGPEKM6.js +145 -0
  582. package/vendor/framey/dist/telemetry-UN55N6U6.js +77 -0
  583. package/vendor/framey/dist/timeline-LHUUMK63.js +1295 -0
  584. package/vendor/framey/dist/tokens-OLECWDFK.js +92 -0
  585. package/vendor/framey/dist/transcribe-O7HQKMIV.js +331 -0
  586. package/vendor/framey/dist/transcribe-UJESPBT5.js +76 -0
  587. package/vendor/framey/dist/tts-YCCQHZD4.js +234 -0
  588. package/vendor/framey/dist/urlDownloader-H2LXLG4U.js +48 -0
  589. package/vendor/framey/dist/usage-HG4WMM7A.js +2167 -0
  590. package/vendor/framey/dist/validate-F2XMN6EV.js +87 -0
  591. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/GSAP-LICENSE.txt +8 -0
  592. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/CustomEase.min.js +11 -0
  593. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/MotionPathPlugin.min.js +11 -0
  594. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/gsap.min.js +11 -0
  595. package/vendor/framey/dist/warmCheck.js +220 -0
  596. package/vendor/framey/dist/warmCheckClient.js +139 -0
  597. package/vendor/framey/dist/workspaceBuildCheck-XYD2YTFW.js +133 -0
  598. package/vendor/framey-player/LICENSE +190 -0
  599. package/vendor/framey-player/NOTICE +16 -0
  600. package/vendor/framey-player/SOURCE.json +6 -0
  601. package/vendor/framey-player/hyperframes-player.global.js +478 -0
  602. package/vendor/framey.pin +1 -0
  603. package/bin/index.js +0 -3
@@ -0,0 +1,460 @@
1
+ import { createReadStream, existsSync, statSync } from "node:fs";
2
+ import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import type { EngineProbes } from "../product/engines.js";
6
+ import { mp4For } from "../project/videos.js";
7
+ import { redact } from "../queue/seal.js";
8
+ import type { Store } from "../queue/store.js";
9
+ import { handlePreview, type PreviewOptions } from "./preview.js";
10
+ import { closeStudios, studioRoute, type StudioRouteOptions } from "./studio-route.js";
11
+ import { engageView, publicationsView, sourcesView, trendsView } from "./screens.js";
12
+ import { buildState } from "./state.js";
13
+ import { topicsRoute } from "./topics-route.js";
14
+ import { creatorsRoute } from "./creators-route.js";
15
+ import { mailRoute } from "./mail-route.js";
16
+ import { createTickRunner, type TickFn } from "./tick-runner.js";
17
+ import * as verbs from "./verbs.js";
18
+
19
+ export interface DashboardOptions {
20
+ store: Store;
21
+ dataDir: string;
22
+ env: Record<string, string | undefined>;
23
+ /** 0 picks a free port. */
24
+ port: number;
25
+ probes?: EngineProbes;
26
+ /** Runs one tick. The CLI passes `runDeskTick`; tests inject a fake. Receives the `desk tick` argv. */
27
+ tick?: TickFn;
28
+ /** The project this desk belongs to. Reported by /api/state so a client can tell which desk it reached. */
29
+ project?: { id: string; name: string } | null;
30
+ /** Renders one frame of a Remotion scene for the Review screen. Defaults to `render --frame`; tests inject a fake. */
31
+ renderFrame?: PreviewOptions["renderFrame"];
32
+ /** Starts a scene studio for "Edit in studio". Defaults to the vendored Framey studio; tests inject a fake. */
33
+ startStudio?: StudioRouteOptions["start"];
34
+ }
35
+
36
+ const here = path.dirname(fileURLToPath(import.meta.url));
37
+ const uiDir = path.join(here, "ui");
38
+ const fontsDir = path.join(here, "fonts");
39
+ const FONT_FILES = new Set(["google-sans-flex-latin.woff2", "jetbrains-mono-medium.woff2"]);
40
+
41
+ const BODY_CAP = 64 * 1024;
42
+ const ID = /^[A-Za-z0-9-]{1,64}$/;
43
+ const PAGES = [/^\/$/, /^\/keys$/, /^\/runs$/, /^\/runs\/[^/]+$/, /^\/series$/, /^\/series\/[^/]+$/, /^\/metrics$/, /^\/warnings$/, /^\/settings$/, /^\/topics$/, /^\/publications$/, /^\/sources$/, /^\/engage$/, /^\/trends$/, /^\/review$/, /^\/creators$/, /^\/mail$/];
44
+ const MIME: Record<string, string> = {
45
+ ".js": "text/javascript; charset=utf-8",
46
+ ".css": "text/css; charset=utf-8",
47
+ ".svg": "image/svg+xml",
48
+ ".html": "text/html; charset=utf-8",
49
+ ".json": "application/json; charset=utf-8",
50
+ ".woff2": "font/woff2",
51
+ ".png": "image/png",
52
+ ".ico": "image/x-icon",
53
+ };
54
+
55
+ type Json = Record<string, unknown>;
56
+
57
+ function sendJson(res: ServerResponse, status: number, body: unknown): void {
58
+ const text = JSON.stringify(body);
59
+ res.writeHead(status, {
60
+ "content-type": "application/json; charset=utf-8",
61
+ "cache-control": "no-store",
62
+ "x-content-type-options": "nosniff",
63
+ });
64
+ res.end(text);
65
+ }
66
+
67
+ function refuse(res: ServerResponse, status: number, refusal: string, fix: string, code?: string): void {
68
+ sendJson(res, status, { ok: false, refusal: redact(refusal), fix: redact(fix), ...(code ? { code } : {}) });
69
+ }
70
+
71
+ const notFound = (res: ServerResponse) => refuse(res, 404, "Not found.", "Check the address, or open the dashboard home.");
72
+
73
+ class BodyError extends Error {
74
+ constructor(
75
+ readonly status: number,
76
+ message: string,
77
+ readonly fix: string,
78
+ ) {
79
+ super(message);
80
+ }
81
+ }
82
+
83
+ function readBody(req: IncomingMessage): Promise<Json> {
84
+ return new Promise((resolve, reject) => {
85
+ const chunks: Buffer[] = [];
86
+ let size = 0;
87
+ let done = false;
88
+ req.on("data", (chunk: Buffer) => {
89
+ if (done) return;
90
+ size += chunk.length;
91
+ if (size > BODY_CAP) {
92
+ done = true;
93
+ chunks.length = 0;
94
+ reject(new BodyError(413, "The request body is too large.", "Send a body under 64 KB."));
95
+ return;
96
+ }
97
+ chunks.push(chunk);
98
+ });
99
+ req.on("end", () => {
100
+ if (done) return;
101
+ done = true;
102
+ const raw = Buffer.concat(chunks).toString("utf8");
103
+ if (!raw.trim()) return resolve({});
104
+ try {
105
+ const parsed: unknown = JSON.parse(raw);
106
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("not an object");
107
+ resolve(parsed as Json);
108
+ } catch {
109
+ reject(new BodyError(400, "The body is not a JSON object.", "Send a JSON object such as {}."));
110
+ }
111
+ });
112
+ req.on("error", () => {
113
+ if (!done) {
114
+ done = true;
115
+ reject(new BodyError(400, "The request body could not be read.", "Send the request again."));
116
+ }
117
+ });
118
+ });
119
+ }
120
+
121
+ const str = (value: unknown): string | undefined => (typeof value === "string" ? value : undefined);
122
+ const bool = (value: unknown): boolean | undefined => (typeof value === "boolean" ? value : undefined);
123
+ const num = (value: unknown): number => (typeof value === "number" ? value : Number.NaN);
124
+ const strList = (value: unknown): string[] | undefined =>
125
+ Array.isArray(value) && value.every((item) => typeof item === "string") ? (value as string[]) : undefined;
126
+
127
+ function decode(segment: string): string | null {
128
+ try {
129
+ return decodeURIComponent(segment);
130
+ } catch {
131
+ return null;
132
+ }
133
+ }
134
+
135
+ function typeOf(file: string): string {
136
+ return MIME[path.extname(file)] ?? "application/octet-stream";
137
+ }
138
+
139
+ function serveWhole(res: ServerResponse, file: string, type: string, extra: Record<string, string> = {}): void {
140
+ if (!existsSync(file)) return notFound(res);
141
+ res.writeHead(200, { "content-type": type, "cache-control": "no-store", "x-content-type-options": "nosniff", ...extra });
142
+ createReadStream(file).pipe(res);
143
+ }
144
+
145
+ function serveMedia(req: IncomingMessage, res: ServerResponse, file: string, type: string): void {
146
+ if (!existsSync(file)) return notFound(res);
147
+ const size = statSync(file).size;
148
+ const match = /^bytes=(\d*)-(\d*)$/.exec(String(req.headers.range ?? ""));
149
+ if (!match || (!match[1] && !match[2])) {
150
+ res.writeHead(200, { "content-type": type, "content-length": size, "accept-ranges": "bytes", "cache-control": "no-store" });
151
+ createReadStream(file).pipe(res);
152
+ return;
153
+ }
154
+ let start: number;
155
+ let end: number;
156
+ if (match[1]) {
157
+ start = Number(match[1]);
158
+ end = match[2] ? Math.min(Number(match[2]), size - 1) : size - 1;
159
+ } else {
160
+ const suffix = Number(match[2]);
161
+ start = Math.max(size - suffix, 0);
162
+ end = size - 1;
163
+ }
164
+ if (size === 0 || start >= size || start > end) {
165
+ res.writeHead(416, { "content-range": `bytes */${size}`, "content-type": "application/json; charset=utf-8" });
166
+ res.end(JSON.stringify({ ok: false, refusal: "That byte range is outside the file.", fix: "Request a range inside the file." }));
167
+ return;
168
+ }
169
+ res.writeHead(206, {
170
+ "content-type": type,
171
+ "content-range": `bytes ${start}-${end}/${size}`,
172
+ "content-length": end - start + 1,
173
+ "accept-ranges": "bytes",
174
+ "cache-control": "no-store",
175
+ });
176
+ createReadStream(file, { start, end }).pipe(res);
177
+ }
178
+
179
+ function verbResponse(res: ServerResponse, result: verbs.VerbResult): void {
180
+ if (!result.ok) return refuse(res, 400, result.refusal, result.fix, result.code);
181
+ sendJson(res, 200, { ok: true, ...(result.data ?? {}) });
182
+ }
183
+
184
+ export async function startDashboard(options: DashboardOptions): Promise<Server> {
185
+ const { store, dataDir, env } = options;
186
+ const probes = options.probes ?? { hyperframesCli: false, comfyReachable: false };
187
+ const userId = env.VFREY_USER ?? "local";
188
+ const tickFn: TickFn =
189
+ options.tick ??
190
+ (async (argv) => {
191
+ const { runDeskTick } = await import("./tick.js");
192
+ return runDeskTick({ store, env: env as Record<string, string | undefined>, argv });
193
+ });
194
+ const runner = createTickRunner(tickFn);
195
+ let boundPort = options.port;
196
+ const studios: StudioRouteOptions["running"] = new Map();
197
+ // A project desk keeps its data in <project>/.vfrey; "Study formats" writes next to it, in <project>/research.
198
+ const projectRoot = options.project ? path.dirname(dataDir) : null;
199
+
200
+ const guard = (req: IncomingMessage, res: ServerResponse): boolean => {
201
+ const hosts = new Set([`127.0.0.1:${boundPort}`, `localhost:${boundPort}`]);
202
+ if (!hosts.has(req.headers.host ?? "")) {
203
+ refuse(res, 403, "Unknown host.", `Open the dashboard at http://127.0.0.1:${boundPort}.`);
204
+ return false;
205
+ }
206
+ if (req.method === "GET" || req.method === "HEAD" || req.method === "OPTIONS") return true;
207
+ if (!String(req.headers["content-type"] ?? "").startsWith("application/json")) {
208
+ refuse(res, 403, "Writes must be JSON.", "Send Content-Type: application/json.");
209
+ return false;
210
+ }
211
+ const origins = new Set([`http://127.0.0.1:${boundPort}`, `http://localhost:${boundPort}`]);
212
+ const origin = req.headers.origin;
213
+ if (origin && !origins.has(origin)) {
214
+ refuse(res, 403, "That origin may not write.", "Use the dashboard page itself.");
215
+ return false;
216
+ }
217
+ const site = req.headers["sec-fetch-site"];
218
+ if (site && site !== "same-origin" && site !== "none") {
219
+ refuse(res, 403, "Cross-site writes are refused.", "Use the dashboard page itself.");
220
+ return false;
221
+ }
222
+ return true;
223
+ };
224
+
225
+ const rendersFile = (id: string, ext: "mp4" | "txt"): string | null => {
226
+ if (!ID.test(id)) return null;
227
+ const known = store.db.prepare(`SELECT payload FROM jobs WHERE id = ?`).get(id) as { payload: string } | undefined;
228
+ if (!known) return null;
229
+ let stored: unknown = null;
230
+ try {
231
+ stored = (JSON.parse(known.payload) as { content?: { outputPath?: unknown } | null }).content?.outputPath ?? null;
232
+ } catch {
233
+ stored = null;
234
+ }
235
+ const mp4 = mp4For(dataDir, id, stored);
236
+ return ext === "mp4" ? mp4 : mp4.replace(/\.mp4$/, ".txt");
237
+ };
238
+
239
+ async function read(req: IncomingMessage, res: ServerResponse, pathname: string): Promise<void> {
240
+ if (pathname.startsWith("/preview/") && (await handlePreview({ store, dataDir, env, project: options.project ?? null, renderFrame: options.renderFrame }, req, res, pathname))) return;
241
+ if (PAGES.some((re) => re.test(pathname))) {
242
+ return serveWhole(res, path.join(uiDir, "index.html"), MIME[".html"]!);
243
+ }
244
+ let m: RegExpExecArray | null;
245
+ if ((m = /^\/assets\/(.+)$/.exec(pathname))) {
246
+ const name = decode(m[1]!);
247
+ if (!name || /[/\\]/.test(name) || name.includes("..") || name.startsWith(".")) return notFound(res);
248
+ const file = path.join(uiDir, name);
249
+ if (!existsSync(file) || !statSync(file).isFile()) return notFound(res);
250
+ return serveWhole(res, file, typeOf(name));
251
+ }
252
+ if ((m = /^\/fonts\/([a-z0-9.-]+\.woff2)$/.exec(pathname)) && FONT_FILES.has(m[1]!)) {
253
+ return serveWhole(res, path.join(fontsDir, m[1]!), "font/woff2", { "cache-control": "public, max-age=86400" });
254
+ }
255
+ if (pathname === "/favicon.ico") {
256
+ return serveWhole(res, path.join(uiDir, "favicon.ico"), MIME[".ico"]!, { "cache-control": "public, max-age=86400" });
257
+ }
258
+ if (pathname === "/api/topics" || pathname === "/api/topics/niches") {
259
+ const reply = await topicsRoute({ env }, pathname, new URL(req.url ?? "/", "http://127.0.0.1").searchParams);
260
+ return reply ? sendJson(res, reply.status, reply.body) : notFound(res);
261
+ }
262
+ if (pathname.startsWith("/api/creators/")) {
263
+ const reply = await creatorsRoute({ env }, { method: "GET", pathname, params: new URL(req.url ?? "/", "http://127.0.0.1").searchParams, body: {}, projectRoot });
264
+ return reply ? sendJson(res, reply.status, reply.body) : notFound(res);
265
+ }
266
+ if (pathname.startsWith("/api/mail/")) {
267
+ const reply = await mailRoute({ env }, { method: "GET", pathname, params: new URL(req.url ?? "/", "http://127.0.0.1").searchParams, body: {} });
268
+ return reply ? sendJson(res, reply.status, reply.body) : notFound(res);
269
+ }
270
+ if (pathname === "/api/publications") return sendJson(res, 200, publicationsView(store, new URL(req.url ?? "/", "http://127.0.0.1").searchParams));
271
+ if (pathname === "/api/sources") return sendJson(res, 200, sourcesView(store));
272
+ if (pathname === "/api/engage") return sendJson(res, 200, engageView(store, new URL(req.url ?? "/", "http://127.0.0.1").searchParams));
273
+ if (pathname === "/api/trends") return sendJson(res, 200, trendsView(store));
274
+ if (pathname === "/api/state") {
275
+ return sendJson(res, 200, await buildState({ store, dataDir, env, userId, tick: runner.status(), probes, project: options.project ?? null }));
276
+ }
277
+ if (pathname === "/api/review") return sendJson(res, 200, { ok: true, jobs: store.reviewQueue(dataDir) });
278
+ if ((m = /^\/api\/runs\/([^/]+)$/.exec(pathname))) {
279
+ const id = decode(m[1]!);
280
+ const detail = id && ID.test(id) ? store.runDetail(id) : null;
281
+ if (!detail) return refuse(res, 404, "No run has that id.", "Open a run from the Runs list.");
282
+ return sendJson(res, 200, { ok: true, run: detail });
283
+ }
284
+ if ((m = /^\/media\/runs\/([^/]+)\.(mp4|txt)$/.exec(pathname))) {
285
+ const ext = m[2] as "mp4" | "txt";
286
+ const id = decode(m[1]!);
287
+ const file = id ? rendersFile(id, ext) : null;
288
+ if (!file) return refuse(res, 404, "No file for that run.", "The render has not produced it.");
289
+ if (ext === "txt") return serveWhole(res, file, "text/plain; charset=utf-8");
290
+ return serveMedia(req, res, file, "video/mp4");
291
+ }
292
+ return notFound(res);
293
+ }
294
+
295
+ async function write(res: ServerResponse, pathname: string, body: Json): Promise<void> {
296
+ let m: RegExpExecArray | null;
297
+ const seg = (value: string | undefined): string => decode(value ?? "") ?? "";
298
+ if (pathname === "/api/creators/watch" || pathname === "/api/creators/study") {
299
+ const reply = await creatorsRoute({ env }, { method: "POST", pathname, params: new URLSearchParams(), body, projectRoot });
300
+ return reply ? sendJson(res, reply.status, reply.body) : notFound(res);
301
+ }
302
+ if (pathname.startsWith("/api/mail/")) {
303
+ const reply = await mailRoute({ env }, { method: "POST", pathname, params: new URLSearchParams(), body });
304
+ return reply ? sendJson(res, reply.status, reply.body) : notFound(res);
305
+ }
306
+ if (pathname === "/api/studio") {
307
+ const reply = await studioRoute({ store, dataDir, env, project: options.project ?? null, running: studios, start: options.startStudio }, body);
308
+ return sendJson(res, reply.status, reply.body);
309
+ }
310
+ if (pathname === "/api/tick") {
311
+ const about = str(body.about)?.trim() || null;
312
+ if (about && about.length > 300) return refuse(res, 400, "The subject is too long.", "Keep it under 300 characters.");
313
+ const started = runner.start({ now: bool(body.now) ?? false, dryRun: bool(body.dryRun) ?? false, series: str(body.series) ?? null, about });
314
+ if (!started.ok) return refuse(res, 409, started.refusal, started.fix);
315
+ return sendJson(res, 202, { ok: true, tick: started.tick });
316
+ }
317
+ if ((m = /^\/api\/jobs\/([^/]+)\/(approve|reject)$/.exec(pathname))) {
318
+ const id = seg(m[1]);
319
+ return verbResponse(res, m[2] === "approve" ? verbs.jobApprove(store, { id }) : verbs.jobReject(store, { id, reason: str(body.reason) }));
320
+ }
321
+ if ((m = /^\/api\/jobs\/([^/]+)\/(retry|cancel)$/.exec(pathname))) {
322
+ const id = seg(m[1]);
323
+ return verbResponse(res, m[2] === "retry" ? verbs.jobRetry(store, { id }) : verbs.jobCancel(store, { id }));
324
+ }
325
+ if (pathname === "/api/series") {
326
+ const id = str(body.id);
327
+ const accountId = str(body.accountId);
328
+ const niche = str(body.niche);
329
+ if (!id || !accountId || niche === undefined) {
330
+ return refuse(res, 400, "A series needs an id, an account, and a niche.", "Send id, accountId, and niche.");
331
+ }
332
+ const result = verbs.seriesCreate(store, { id, accountId, niche, language: str(body.language), timezone: str(body.timezone), engine: str(body.engine) });
333
+ return verbResponse(res, result.ok ? { ok: true, data: { id } } : result);
334
+ }
335
+ if ((m = /^\/api\/series\/([^/]+)\/(pause|resume|delete|cadence|quota|platforms|sources|live|dry-run)$/.exec(pathname))) {
336
+ const id = seg(m[1]);
337
+ switch (m[2]) {
338
+ case "pause":
339
+ return verbResponse(res, verbs.seriesPause(store, { id }));
340
+ case "resume":
341
+ return verbResponse(res, verbs.seriesResume(store, { id }));
342
+ case "delete":
343
+ return verbResponse(res, verbs.seriesDelete(store, { id }));
344
+ case "live":
345
+ return verbResponse(res, verbs.seriesLive(store, { id, confirm: str(body.confirm), key: env.CREDENTIALS_KEY }));
346
+ case "dry-run":
347
+ return verbResponse(res, verbs.seriesOff(store, { id }));
348
+ case "cadence": {
349
+ const windows = strList(body.windows);
350
+ const timezone = str(body.timezone);
351
+ if (!timezone || !windows) return refuse(res, 400, "Cadence needs a timezone and windows.", "Send timezone, minHoursBetweenPosts, maxPostsPerDay, and windows.");
352
+ return verbResponse(res, verbs.cadenceSet(store, { seriesId: id, timezone, minHoursBetweenPosts: num(body.minHoursBetweenPosts), maxPostsPerDay: num(body.maxPostsPerDay), windows }));
353
+ }
354
+ case "quota": {
355
+ const current = (store.snapshot(userId).series as Array<Record<string, unknown>>).find((s) => s.id === id);
356
+ if (!current) return refuse(res, 400, `Series ${id} was not found.`, "Check the series id on the Series page.");
357
+ const pick = (key: string): number => (key in body ? num(body[key]) : Number(current[key]));
358
+ return verbResponse(res, verbs.quotaSet(store, { seriesId: id, youtubeDailyUploadCap: pick("youtubeDailyUploadCap"), maxRepliesPerDay: pick("maxRepliesPerDay"), minSecondsBetween: pick("minSecondsBetween") }));
359
+ }
360
+ case "platforms": {
361
+ const platform = str(body.platform);
362
+ const enabled = bool(body.enabled);
363
+ if (!platform || enabled === undefined) return refuse(res, 400, "A platform needs a name and an enabled flag.", "Send platform and enabled.");
364
+ const config = body.config && typeof body.config === "object" && !Array.isArray(body.config) ? (body.config as Json) : undefined;
365
+ return verbResponse(res, verbs.platformSet(store, { seriesId: id, platform, enabled, config }));
366
+ }
367
+ default: {
368
+ const kind = str(body.kind);
369
+ const value = str(body.value);
370
+ if (!kind || value === undefined) return refuse(res, 400, "A source needs a kind and a value.", "Send kind and value.");
371
+ const result = verbs.sourceAdd(store, { seriesId: id, kind, value });
372
+ return verbResponse(res, result.ok ? { ok: true, data: { id: result.data?.id } } : result);
373
+ }
374
+ }
375
+ }
376
+ if ((m = /^\/api\/series\/([^/]+)$/.exec(pathname))) {
377
+ const patch = {
378
+ niche: str(body.niche),
379
+ language: str(body.language),
380
+ voices: strList(body.voices),
381
+ hashtagPool: strList(body.hashtagPool),
382
+ autopilot: bool(body.autopilot),
383
+ engageEnabled: bool(body.engageEnabled),
384
+ publishEnabled: bool(body.publishEnabled),
385
+ reviewEnabled: bool(body.reviewEnabled),
386
+ allowSpicy: bool(body.allowSpicy),
387
+ stylePath: str(body.stylePath),
388
+ engine: str(body.engine),
389
+ };
390
+ return verbResponse(res, verbs.seriesUpdate(store, { id: seg(m[1]), patch }));
391
+ }
392
+ if ((m = /^\/api\/sources\/([^/]+)\/(update|delete)$/.exec(pathname))) {
393
+ const id = seg(m[1]);
394
+ if (m[2] === "delete") return verbResponse(res, verbs.sourceRemove(store, { id }));
395
+ const value = str(body.value);
396
+ if (value === undefined) return refuse(res, 400, "A source update needs a value.", "Send value.");
397
+ return verbResponse(res, verbs.sourceUpdate(store, { id, value }));
398
+ }
399
+ if (pathname === "/api/secrets" || pathname === "/api/secrets/clear") {
400
+ const ownerKind = str(body.ownerKind);
401
+ const ownerId = str(body.ownerId);
402
+ const field = str(body.field);
403
+ if ((ownerKind !== "account" && ownerKind !== "series") || !ownerId || !field) {
404
+ return refuse(res, 400, "A secret needs an owner kind, an owner id, and a field.", "Send ownerKind (account or series), ownerId, and field.");
405
+ }
406
+ if (pathname === "/api/secrets/clear") return verbResponse(res, verbs.secretClear(store, { ownerKind, ownerId, field }));
407
+ const value = str(body.value);
408
+ if (value === undefined) return refuse(res, 400, "A secret needs a value.", "Send value, or an empty string to leave it unchanged.");
409
+ return verbResponse(res, verbs.secretSet(store, { ownerKind, ownerId, field, value, key: env.CREDENTIALS_KEY }));
410
+ }
411
+ if ((m = /^\/api\/warnings\/([^/]+)\/ack$/.exec(pathname))) {
412
+ return verbResponse(res, verbs.warningAck(store, { id: seg(m[1]) }));
413
+ }
414
+ if (pathname === "/api/mode") {
415
+ const mode = str(body.mode) ?? "";
416
+ return verbResponse(res, verbs.modeSet(store, { userId, mode, llmKeyPresent: Boolean(env.LLM_API_KEY) }));
417
+ }
418
+ return notFound(res);
419
+ }
420
+
421
+ const server = createServer((req, res) => {
422
+ handle(req, res).catch((error: unknown) => {
423
+ if (error instanceof BodyError) {
424
+ if (!res.headersSent) refuse(res, error.status, error.message, error.fix);
425
+ if (error.status === 413) req.resume();
426
+ return;
427
+ }
428
+ if (!res.headersSent) refuse(res, 500, "The dashboard hit an error.", "Try again, or restart `vfrey dashboard`.");
429
+ else res.end();
430
+ });
431
+ });
432
+
433
+ async function handle(req: IncomingMessage, res: ServerResponse): Promise<void> {
434
+ if (!guard(req, res)) return;
435
+ const method = req.method ?? "GET";
436
+ if (method === "OPTIONS") return refuse(res, 405, "OPTIONS is not supported.", "Use GET or POST from the dashboard page.");
437
+ const pathname = new URL(req.url ?? "/", "http://127.0.0.1").pathname;
438
+ if (method === "GET" || method === "HEAD") return read(req, res, pathname);
439
+ if (method !== "POST") return refuse(res, 405, `${method} is not supported.`, "Use GET or POST.");
440
+ const body = await readBody(req);
441
+ try {
442
+ return await write(res, pathname, body);
443
+ } catch (error) {
444
+ if (res.headersSent) throw error;
445
+ const message = error instanceof Error ? error.message : "The change failed.";
446
+ return refuse(res, 400, message.split("\n")[0] ?? "The change failed.", "Check the values and try again.");
447
+ }
448
+ }
449
+
450
+ server.on("close", () => void closeStudios(studios));
451
+ return new Promise<Server>((resolve, reject) => {
452
+ server.once("error", reject);
453
+ server.listen(options.port, "127.0.0.1", () => {
454
+ server.off("error", reject);
455
+ const address = server.address();
456
+ boundPort = address && typeof address === "object" ? address.port : options.port;
457
+ resolve(server);
458
+ });
459
+ });
460
+ }
@@ -0,0 +1,103 @@
1
+ // The dashboard's window onto vfrey Mail. The browser asks this server; this server asks the vfrey API with the stored token.
2
+ // The token is read here, per request, and is never put in a response. Approve and Send are separate routes, and each
3
+ // needs `confirm: true` from the browser's inline confirm: one draft at a time, never chained.
4
+ import { ApiFailure, clean } from "../api/client.js";
5
+ import { ApiConfigError } from "../api/config.js";
6
+ import { MAIL_KINDS, MAIL_STATUSES } from "../api/types.js";
7
+ import {
8
+ approveMailDraft,
9
+ createMailDraft,
10
+ discardMailDraft,
11
+ editMailDraft,
12
+ getMailDraft,
13
+ getMailIdentity,
14
+ listMailDrafts,
15
+ saveMailIdentity,
16
+ sendMailDraft,
17
+ type MailDeps,
18
+ } from "../api/mail.js";
19
+ import { CREATOR_PLATFORM_IDS, normalizeHandle } from "../api/creators.js";
20
+ import type { MailDraftBody, MailDraftPatch } from "../api/types.js";
21
+ import { redact } from "../queue/seal.js";
22
+ import type { RouteReply } from "./creators-route.js";
23
+
24
+ export interface MailRouteInput {
25
+ method: "GET" | "POST";
26
+ pathname: string;
27
+ params: URLSearchParams;
28
+ body: Record<string, unknown>;
29
+ }
30
+
31
+ const bad = (refusal: string, fix: string): RouteReply => ({ status: 400, body: { ok: false, refusal, fix, code: "bad_request" } });
32
+ const str = (v: unknown): string => (typeof v === "string" ? v.trim() : "");
33
+
34
+ function refusal(error: unknown): RouteReply {
35
+ if (error instanceof ApiFailure || error instanceof ApiConfigError) {
36
+ const status = error instanceof ApiFailure ? error.status : undefined;
37
+ const code = error instanceof ApiFailure ? (error.code ?? "api_error") : "api_config";
38
+ const http = code === "not_live" || code === "not_signed_in" ? 401 : status && status >= 400 && status < 600 ? status : 502;
39
+ return { status: http, body: { ok: false, refusal: redact(error.message), fix: redact(error.fix), code, loginCommand: code === "not_signed_in" || code === "unauthorized" || code === "not_live" ? "npx vfrey login" : undefined } };
40
+ }
41
+ return { status: 502, body: { ok: false, refusal: redact(error instanceof Error ? clean(error.message) : "Mail could not be read."), fix: "Try again in a moment.", code: "api_error" } };
42
+ }
43
+
44
+ /** GET /api/mail/{drafts,draft,identity}; POST /api/mail/{draft,edit,approve,send,discard,identity}. Null for any other path. */
45
+ export async function mailRoute(deps: MailDeps, input: MailRouteInput): Promise<RouteReply | null> {
46
+ const { method, pathname, params, body } = input;
47
+ try {
48
+ if (method === "GET" && pathname === "/api/mail/drafts") {
49
+ const status = params.get("status")?.trim().toLowerCase() || undefined;
50
+ if (status && !(MAIL_STATUSES as readonly string[]).includes(status)) return bad("Pick a status from the list.", "Choose one from the filter.");
51
+ return { status: 200, body: { ok: true, drafts: await listMailDrafts(deps, status ? { status } : {}) } };
52
+ }
53
+ if (method === "GET" && pathname === "/api/mail/draft") {
54
+ const id = params.get("id")?.trim() ?? "";
55
+ if (!id) return bad("Pick a draft.", "Choose one from the list.");
56
+ return { status: 200, body: { ok: true, draft: await getMailDraft(deps, id) } };
57
+ }
58
+ if (method === "GET" && pathname === "/api/mail/identity") return { status: 200, body: { ok: true, identity: await getMailIdentity(deps) } };
59
+ if (method !== "POST") return null;
60
+
61
+ if (pathname === "/api/mail/draft") {
62
+ const kind = str(body.kind).toLowerCase();
63
+ if (!(MAIL_KINDS as readonly string[]).includes(kind)) return bad("Pick what kind of message.", "Choose sponsor, brand, creator collab or client update.");
64
+ const need = str(body.need);
65
+ if (!need) return bad("Say what the message should ask or say.", "Fill in the \"What should it say\" box.");
66
+ const draft: MailDraftBody = { kind, need };
67
+ if (str(body.to)) draft.to = str(body.to);
68
+ if (str(body.toName)) draft.toName = str(body.toName);
69
+ if (str(body.tone)) draft.tone = str(body.tone);
70
+ if (str(body.handle)) {
71
+ const platform = str(body.platform).toLowerCase();
72
+ const handle = normalizeHandle(str(body.handle));
73
+ if (!(CREATOR_PLATFORM_IDS as readonly string[]).includes(platform) || !/^[a-z0-9._-]{1,80}$/i.test(handle)) return bad("The creator needs a platform and a handle.", "Pick a platform and type a handle like ana.cooks.");
74
+ draft.creator = { platform, handle };
75
+ }
76
+ return { status: 200, body: { ok: true, draft: await createMailDraft(deps, draft) } };
77
+ }
78
+ if (pathname === "/api/mail/identity") {
79
+ const input = { fromName: str(body.fromName) || undefined, replyTo: str(body.replyTo).toLowerCase() || undefined, postalAddress: str(body.postalAddress) || undefined, org: typeof body.org === "string" ? body.org.trim() : undefined };
80
+ return { status: 200, body: { ok: true, ...(await saveMailIdentity(deps, input)) } };
81
+ }
82
+ if (pathname === "/api/mail/edit" || pathname === "/api/mail/approve" || pathname === "/api/mail/send" || pathname === "/api/mail/discard") {
83
+ const id = str(body.id);
84
+ if (!id) return bad("Pick a draft.", "Choose one from the list.");
85
+ if (pathname === "/api/mail/edit") {
86
+ const patch: MailDraftPatch = {};
87
+ if (typeof body.to === "string") patch.to = body.to.trim();
88
+ if (typeof body.toName === "string") patch.toName = body.toName.trim() || null;
89
+ if (typeof body.subject === "string") patch.subject = body.subject.trim();
90
+ if (typeof body.text === "string") patch.text = body.text;
91
+ if (Object.keys(patch).length === 0) return bad("Nothing to change.", "Edit the recipient, subject or text first.");
92
+ return { status: 200, body: { ok: true, draft: await editMailDraft(deps, id, patch) } };
93
+ }
94
+ if (pathname === "/api/mail/discard") return { status: 200, body: { ok: true, draft: await discardMailDraft(deps, id) } };
95
+ // Approve and Send only run for a click the person confirmed, on the one draft named, nothing else.
96
+ if (body.confirm !== true) return bad("Confirm first.", "Press the button again and confirm the recipient and subject.");
97
+ return { status: 200, body: { ok: true, draft: pathname === "/api/mail/approve" ? await approveMailDraft(deps, id) : await sendMailDraft(deps, id) } };
98
+ }
99
+ return null;
100
+ } catch (error) {
101
+ return refusal(error);
102
+ }
103
+ }