@dxos/plugin-magazine 0.0.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 (390) hide show
  1. package/LICENSE +105 -0
  2. package/PLUGIN.mdl +653 -0
  3. package/dist/lib/neutral/FeedArticle-PEWCFFSP.mjs +90 -0
  4. package/dist/lib/neutral/FeedArticle-PEWCFFSP.mjs.map +7 -0
  5. package/dist/lib/neutral/FeedProperties-EIO7IZOA.mjs +97 -0
  6. package/dist/lib/neutral/FeedProperties-EIO7IZOA.mjs.map +7 -0
  7. package/dist/lib/neutral/MagazineArticle-DEUTR23F.mjs +325 -0
  8. package/dist/lib/neutral/MagazineArticle-DEUTR23F.mjs.map +7 -0
  9. package/dist/lib/neutral/MagazinePlugin.mjs +62 -0
  10. package/dist/lib/neutral/MagazinePlugin.mjs.map +7 -0
  11. package/dist/lib/neutral/MagazinePlugin.workerd.mjs +12 -0
  12. package/dist/lib/neutral/MagazinePlugin.workerd.mjs.map +7 -0
  13. package/dist/lib/neutral/PostArticle-RERHXPXC.mjs +230 -0
  14. package/dist/lib/neutral/PostArticle-RERHXPXC.mjs.map +7 -0
  15. package/dist/lib/neutral/PostCard-VDGN7BBA.mjs +60 -0
  16. package/dist/lib/neutral/PostCard-VDGN7BBA.mjs.map +7 -0
  17. package/dist/lib/neutral/app-graph-builder-JDPRBVHL.mjs +121 -0
  18. package/dist/lib/neutral/app-graph-builder-JDPRBVHL.mjs.map +7 -0
  19. package/dist/lib/neutral/capabilities/index.mjs +21 -0
  20. package/dist/lib/neutral/capabilities/index.mjs.map +7 -0
  21. package/dist/lib/neutral/chunk-3K4AQKBZ.mjs +12 -0
  22. package/dist/lib/neutral/chunk-3K4AQKBZ.mjs.map +7 -0
  23. package/dist/lib/neutral/chunk-AFPBWBTK.mjs +591 -0
  24. package/dist/lib/neutral/chunk-AFPBWBTK.mjs.map +7 -0
  25. package/dist/lib/neutral/chunk-APQRTYT3.mjs +57 -0
  26. package/dist/lib/neutral/chunk-APQRTYT3.mjs.map +7 -0
  27. package/dist/lib/neutral/chunk-GU725ZF4.mjs +11 -0
  28. package/dist/lib/neutral/chunk-GU725ZF4.mjs.map +7 -0
  29. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
  30. package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +7 -0
  31. package/dist/lib/neutral/chunk-JFPR6OV4.mjs +60 -0
  32. package/dist/lib/neutral/chunk-JFPR6OV4.mjs.map +7 -0
  33. package/dist/lib/neutral/chunk-KTYLUOUG.mjs +36 -0
  34. package/dist/lib/neutral/chunk-KTYLUOUG.mjs.map +7 -0
  35. package/dist/lib/neutral/chunk-MBPX7RPI.mjs +14 -0
  36. package/dist/lib/neutral/chunk-MBPX7RPI.mjs.map +7 -0
  37. package/dist/lib/neutral/chunk-MJ2AY2CK.mjs +8 -0
  38. package/dist/lib/neutral/chunk-MJ2AY2CK.mjs.map +7 -0
  39. package/dist/lib/neutral/chunk-OYCDRSWB.mjs +624 -0
  40. package/dist/lib/neutral/chunk-OYCDRSWB.mjs.map +7 -0
  41. package/dist/lib/neutral/chunk-SAWKLGJU.mjs +28 -0
  42. package/dist/lib/neutral/chunk-SAWKLGJU.mjs.map +7 -0
  43. package/dist/lib/neutral/chunk-XCNCK47N.mjs +15 -0
  44. package/dist/lib/neutral/chunk-XCNCK47N.mjs.map +7 -0
  45. package/dist/lib/neutral/clear-magazine-KNY4OWY4.mjs +59 -0
  46. package/dist/lib/neutral/clear-magazine-KNY4OWY4.mjs.map +7 -0
  47. package/dist/lib/neutral/components/index.mjs +338 -0
  48. package/dist/lib/neutral/components/index.mjs.map +7 -0
  49. package/dist/lib/neutral/containers/index.mjs +17 -0
  50. package/dist/lib/neutral/containers/index.mjs.map +7 -0
  51. package/dist/lib/neutral/create-object-SYZMRR4H.mjs +98 -0
  52. package/dist/lib/neutral/create-object-SYZMRR4H.mjs.map +7 -0
  53. package/dist/lib/neutral/curate-magazine-MA6OLK2X.mjs +184 -0
  54. package/dist/lib/neutral/curate-magazine-MA6OLK2X.mjs.map +7 -0
  55. package/dist/lib/neutral/fetch-article-content-CZO6D3QD.mjs +31 -0
  56. package/dist/lib/neutral/fetch-article-content-CZO6D3QD.mjs.map +7 -0
  57. package/dist/lib/neutral/index.mjs +18 -0
  58. package/dist/lib/neutral/index.mjs.map +7 -0
  59. package/dist/lib/neutral/load-post-content-XGL6VFF6.mjs +65 -0
  60. package/dist/lib/neutral/load-post-content-XGL6VFF6.mjs.map +7 -0
  61. package/dist/lib/neutral/meta.json +1 -0
  62. package/dist/lib/neutral/meta.mjs +8 -0
  63. package/dist/lib/neutral/meta.mjs.map +7 -0
  64. package/dist/lib/neutral/navigation-resolver-IN4OMCDR.mjs +18 -0
  65. package/dist/lib/neutral/navigation-resolver-IN4OMCDR.mjs.map +7 -0
  66. package/dist/lib/neutral/operation-handler-JE3743BU.mjs +8 -0
  67. package/dist/lib/neutral/operation-handler-JE3743BU.mjs.map +7 -0
  68. package/dist/lib/neutral/operations/index.mjs +8 -0
  69. package/dist/lib/neutral/operations/index.mjs.map +7 -0
  70. package/dist/lib/neutral/plugin.mjs +16 -0
  71. package/dist/lib/neutral/plugin.mjs.map +7 -0
  72. package/dist/lib/neutral/plugin.workerd.mjs +16 -0
  73. package/dist/lib/neutral/plugin.workerd.mjs.map +7 -0
  74. package/dist/lib/neutral/react-surface-TZESAMG2.mjs +60 -0
  75. package/dist/lib/neutral/react-surface-TZESAMG2.mjs.map +7 -0
  76. package/dist/lib/neutral/routine-templates-A2XTJOZO.mjs +52 -0
  77. package/dist/lib/neutral/routine-templates-A2XTJOZO.mjs.map +7 -0
  78. package/dist/lib/neutral/skill-definition-3KLMUEC5.mjs +8 -0
  79. package/dist/lib/neutral/skill-definition-3KLMUEC5.mjs.map +7 -0
  80. package/dist/lib/neutral/skills/index.mjs +46 -0
  81. package/dist/lib/neutral/skills/index.mjs.map +7 -0
  82. package/dist/lib/neutral/sync-feed-UZX2XHSQ.mjs +99 -0
  83. package/dist/lib/neutral/sync-feed-UZX2XHSQ.mjs.map +7 -0
  84. package/dist/lib/neutral/translations.mjs +81 -0
  85. package/dist/lib/neutral/translations.mjs.map +7 -0
  86. package/dist/lib/neutral/types/index.mjs +14 -0
  87. package/dist/lib/neutral/types/index.mjs.map +7 -0
  88. package/dist/types/dx.config.d.ts +28 -0
  89. package/dist/types/dx.config.d.ts.map +1 -0
  90. package/dist/types/src/MagazinePlugin.d.ts +4 -0
  91. package/dist/types/src/MagazinePlugin.d.ts.map +1 -0
  92. package/dist/types/src/MagazinePlugin.test.d.ts +2 -0
  93. package/dist/types/src/MagazinePlugin.test.d.ts.map +1 -0
  94. package/dist/types/src/MagazinePlugin.workerd.d.ts +4 -0
  95. package/dist/types/src/MagazinePlugin.workerd.d.ts.map +1 -0
  96. package/dist/types/src/atoms/atoms.test.d.ts +2 -0
  97. package/dist/types/src/atoms/atoms.test.d.ts.map +1 -0
  98. package/dist/types/src/atoms/index.d.ts +7 -0
  99. package/dist/types/src/atoms/index.d.ts.map +1 -0
  100. package/dist/types/src/atoms/magazine-posts.d.ts +16 -0
  101. package/dist/types/src/atoms/magazine-posts.d.ts.map +1 -0
  102. package/dist/types/src/atoms/post-content.d.ts +7 -0
  103. package/dist/types/src/atoms/post-content.d.ts.map +1 -0
  104. package/dist/types/src/atoms/post-curation.d.ts +16 -0
  105. package/dist/types/src/atoms/post-curation.d.ts.map +1 -0
  106. package/dist/types/src/atoms/post-display.d.ts +22 -0
  107. package/dist/types/src/atoms/post-display.d.ts.map +1 -0
  108. package/dist/types/src/atoms/post-read.d.ts +14 -0
  109. package/dist/types/src/atoms/post-read.d.ts.map +1 -0
  110. package/dist/types/src/atoms/post-tags.d.ts +16 -0
  111. package/dist/types/src/atoms/post-tags.d.ts.map +1 -0
  112. package/dist/types/src/capabilities/app-graph-builder.d.ts +5 -0
  113. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -0
  114. package/dist/types/src/capabilities/create-object.d.ts +5 -0
  115. package/dist/types/src/capabilities/create-object.d.ts.map +1 -0
  116. package/dist/types/src/capabilities/index.d.ts +10 -0
  117. package/dist/types/src/capabilities/index.d.ts.map +1 -0
  118. package/dist/types/src/capabilities/navigation-resolver.d.ts +5 -0
  119. package/dist/types/src/capabilities/navigation-resolver.d.ts.map +1 -0
  120. package/dist/types/src/capabilities/operation-handler.d.ts +6 -0
  121. package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
  122. package/dist/types/src/capabilities/react-surface.d.ts +5 -0
  123. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
  124. package/dist/types/src/capabilities/routine-templates.d.ts +5 -0
  125. package/dist/types/src/capabilities/routine-templates.d.ts.map +1 -0
  126. package/dist/types/src/capabilities/skill-definition.d.ts +5 -0
  127. package/dist/types/src/capabilities/skill-definition.d.ts.map +1 -0
  128. package/dist/types/src/components/PostContent/PostContent.d.ts +30 -0
  129. package/dist/types/src/components/PostContent/PostContent.d.ts.map +1 -0
  130. package/dist/types/src/components/PostContent/PostContent.test.d.ts +2 -0
  131. package/dist/types/src/components/PostContent/PostContent.test.d.ts.map +1 -0
  132. package/dist/types/src/components/PostContent/index.d.ts +2 -0
  133. package/dist/types/src/components/PostContent/index.d.ts.map +1 -0
  134. package/dist/types/src/components/PostStack/PostStack.d.ts +17 -0
  135. package/dist/types/src/components/PostStack/PostStack.d.ts.map +1 -0
  136. package/dist/types/src/components/PostStack/PostStack.stories.d.ts +10 -0
  137. package/dist/types/src/components/PostStack/PostStack.stories.d.ts.map +1 -0
  138. package/dist/types/src/components/PostStack/index.d.ts +2 -0
  139. package/dist/types/src/components/PostStack/index.d.ts.map +1 -0
  140. package/dist/types/src/components/SubscriptionStack/SubscriptionStack.d.ts +23 -0
  141. package/dist/types/src/components/SubscriptionStack/SubscriptionStack.d.ts.map +1 -0
  142. package/dist/types/src/components/SubscriptionStack/SubscriptionStack.stories.d.ts +8 -0
  143. package/dist/types/src/components/SubscriptionStack/SubscriptionStack.stories.d.ts.map +1 -0
  144. package/dist/types/src/components/SubscriptionStack/index.d.ts +2 -0
  145. package/dist/types/src/components/SubscriptionStack/index.d.ts.map +1 -0
  146. package/dist/types/src/components/index.d.ts +4 -0
  147. package/dist/types/src/components/index.d.ts.map +1 -0
  148. package/dist/types/src/containers/FeedArticle/FeedArticle.d.ts +6 -0
  149. package/dist/types/src/containers/FeedArticle/FeedArticle.d.ts.map +1 -0
  150. package/dist/types/src/containers/FeedArticle/FeedArticle.stories.d.ts +12 -0
  151. package/dist/types/src/containers/FeedArticle/FeedArticle.stories.d.ts.map +1 -0
  152. package/dist/types/src/containers/FeedArticle/FeedToolbar.d.ts +10 -0
  153. package/dist/types/src/containers/FeedArticle/FeedToolbar.d.ts.map +1 -0
  154. package/dist/types/src/containers/FeedArticle/index.d.ts +2 -0
  155. package/dist/types/src/containers/FeedArticle/index.d.ts.map +1 -0
  156. package/dist/types/src/containers/FeedProperties/FeedProperties.d.ts +6 -0
  157. package/dist/types/src/containers/FeedProperties/FeedProperties.d.ts.map +1 -0
  158. package/dist/types/src/containers/FeedProperties/index.d.ts +2 -0
  159. package/dist/types/src/containers/FeedProperties/index.d.ts.map +1 -0
  160. package/dist/types/src/containers/MagazineArticle/MagazineArticle.d.ts +6 -0
  161. package/dist/types/src/containers/MagazineArticle/MagazineArticle.d.ts.map +1 -0
  162. package/dist/types/src/containers/MagazineArticle/MagazineArticle.stories.d.ts +9 -0
  163. package/dist/types/src/containers/MagazineArticle/MagazineArticle.stories.d.ts.map +1 -0
  164. package/dist/types/src/containers/MagazineArticle/MagazineTile.d.ts +11 -0
  165. package/dist/types/src/containers/MagazineArticle/MagazineTile.d.ts.map +1 -0
  166. package/dist/types/src/containers/MagazineArticle/index.d.ts +2 -0
  167. package/dist/types/src/containers/MagazineArticle/index.d.ts.map +1 -0
  168. package/dist/types/src/containers/MagazineArticle/useToolbar.d.ts +18 -0
  169. package/dist/types/src/containers/MagazineArticle/useToolbar.d.ts.map +1 -0
  170. package/dist/types/src/containers/PostArticle/PostArticle.d.ts +6 -0
  171. package/dist/types/src/containers/PostArticle/PostArticle.d.ts.map +1 -0
  172. package/dist/types/src/containers/PostArticle/PostArticle.stories.d.ts +8 -0
  173. package/dist/types/src/containers/PostArticle/PostArticle.stories.d.ts.map +1 -0
  174. package/dist/types/src/containers/PostArticle/PostToolbar.d.ts +19 -0
  175. package/dist/types/src/containers/PostArticle/PostToolbar.d.ts.map +1 -0
  176. package/dist/types/src/containers/PostArticle/index.d.ts +2 -0
  177. package/dist/types/src/containers/PostArticle/index.d.ts.map +1 -0
  178. package/dist/types/src/containers/PostCard/PostCard.d.ts +11 -0
  179. package/dist/types/src/containers/PostCard/PostCard.d.ts.map +1 -0
  180. package/dist/types/src/containers/PostCard/index.d.ts +2 -0
  181. package/dist/types/src/containers/PostCard/index.d.ts.map +1 -0
  182. package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.d.ts +5 -0
  183. package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.d.ts.map +1 -0
  184. package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.stories.d.ts +8 -0
  185. package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.stories.d.ts.map +1 -0
  186. package/dist/types/src/containers/SubscriptionsArticle/index.d.ts +2 -0
  187. package/dist/types/src/containers/SubscriptionsArticle/index.d.ts.map +1 -0
  188. package/dist/types/src/containers/index.d.ts +7 -0
  189. package/dist/types/src/containers/index.d.ts.map +1 -0
  190. package/dist/types/src/index.d.ts +3 -0
  191. package/dist/types/src/index.d.ts.map +1 -0
  192. package/dist/types/src/meta.d.ts +33 -0
  193. package/dist/types/src/meta.d.ts.map +1 -0
  194. package/dist/types/src/operations/clear-magazine.d.ts +10 -0
  195. package/dist/types/src/operations/clear-magazine.d.ts.map +1 -0
  196. package/dist/types/src/operations/curate-magazine.d.ts +25 -0
  197. package/dist/types/src/operations/curate-magazine.d.ts.map +1 -0
  198. package/dist/types/src/operations/curate-magazine.skill.test.d.ts +2 -0
  199. package/dist/types/src/operations/curate-magazine.skill.test.d.ts.map +1 -0
  200. package/dist/types/src/operations/curate-magazine.test.d.ts +2 -0
  201. package/dist/types/src/operations/curate-magazine.test.d.ts.map +1 -0
  202. package/dist/types/src/operations/extraction/article.d.ts +37 -0
  203. package/dist/types/src/operations/extraction/article.d.ts.map +1 -0
  204. package/dist/types/src/operations/extraction/article.test.d.ts +2 -0
  205. package/dist/types/src/operations/extraction/article.test.d.ts.map +1 -0
  206. package/dist/types/src/operations/extraction/index.d.ts +2 -0
  207. package/dist/types/src/operations/extraction/index.d.ts.map +1 -0
  208. package/dist/types/src/operations/fetch-article-content.d.ts +5 -0
  209. package/dist/types/src/operations/fetch-article-content.d.ts.map +1 -0
  210. package/dist/types/src/operations/index.d.ts +3 -0
  211. package/dist/types/src/operations/index.d.ts.map +1 -0
  212. package/dist/types/src/operations/load-post-content.d.ts +8 -0
  213. package/dist/types/src/operations/load-post-content.d.ts.map +1 -0
  214. package/dist/types/src/operations/sources/article.d.ts +24 -0
  215. package/dist/types/src/operations/sources/article.d.ts.map +1 -0
  216. package/dist/types/src/operations/sources/cors.d.ts +12 -0
  217. package/dist/types/src/operations/sources/cors.d.ts.map +1 -0
  218. package/dist/types/src/operations/sources/feed-fetcher.d.ts +22 -0
  219. package/dist/types/src/operations/sources/feed-fetcher.d.ts.map +1 -0
  220. package/dist/types/src/operations/sources/feed-fetcher.test.d.ts +2 -0
  221. package/dist/types/src/operations/sources/feed-fetcher.test.d.ts.map +1 -0
  222. package/dist/types/src/operations/sources/http.d.ts +9 -0
  223. package/dist/types/src/operations/sources/http.d.ts.map +1 -0
  224. package/dist/types/src/operations/sources/index.d.ts +6 -0
  225. package/dist/types/src/operations/sources/index.d.ts.map +1 -0
  226. package/dist/types/src/operations/sources/rss.d.ts +4 -0
  227. package/dist/types/src/operations/sources/rss.d.ts.map +1 -0
  228. package/dist/types/src/operations/sources/rss.test.d.ts +2 -0
  229. package/dist/types/src/operations/sources/rss.test.d.ts.map +1 -0
  230. package/dist/types/src/operations/sources/standard-site.d.ts +33 -0
  231. package/dist/types/src/operations/sources/standard-site.d.ts.map +1 -0
  232. package/dist/types/src/operations/sync-feed.d.ts +5 -0
  233. package/dist/types/src/operations/sync-feed.d.ts.map +1 -0
  234. package/dist/types/src/operations/sync-feed.test.d.ts +2 -0
  235. package/dist/types/src/operations/sync-feed.test.d.ts.map +1 -0
  236. package/dist/types/src/operations/util.d.ts +25 -0
  237. package/dist/types/src/operations/util.d.ts.map +1 -0
  238. package/dist/types/src/operations/util.test.d.ts +2 -0
  239. package/dist/types/src/operations/util.test.d.ts.map +1 -0
  240. package/dist/types/src/paths.d.ts +3 -0
  241. package/dist/types/src/paths.d.ts.map +1 -0
  242. package/dist/types/src/plugin.d.ts +4 -0
  243. package/dist/types/src/plugin.d.ts.map +1 -0
  244. package/dist/types/src/plugin.workerd.d.ts +3 -0
  245. package/dist/types/src/plugin.workerd.d.ts.map +1 -0
  246. package/dist/types/src/skills/index.d.ts +2 -0
  247. package/dist/types/src/skills/index.d.ts.map +1 -0
  248. package/dist/types/src/skills/magazine-skill.d.ts +4 -0
  249. package/dist/types/src/skills/magazine-skill.d.ts.map +1 -0
  250. package/dist/types/src/stories/ArticleExtractor.stories.d.ts +267 -0
  251. package/dist/types/src/stories/ArticleExtractor.stories.d.ts.map +1 -0
  252. package/dist/types/src/stories/MagazineCurate.stories.d.ts +32 -0
  253. package/dist/types/src/stories/MagazineCurate.stories.d.ts.map +1 -0
  254. package/dist/types/src/stories/theregister-fixture.test.d.ts +2 -0
  255. package/dist/types/src/stories/theregister-fixture.test.d.ts.map +1 -0
  256. package/dist/types/src/templates/magazine-curation.d.ts +7 -0
  257. package/dist/types/src/templates/magazine-curation.d.ts.map +1 -0
  258. package/dist/types/src/testing/builder.d.ts +28 -0
  259. package/dist/types/src/testing/builder.d.ts.map +1 -0
  260. package/dist/types/src/testing/index.d.ts +2 -0
  261. package/dist/types/src/testing/index.d.ts.map +1 -0
  262. package/dist/types/src/translations.d.ts +247 -0
  263. package/dist/types/src/translations.d.ts.map +1 -0
  264. package/dist/types/src/types/CreateSubscription.d.ts +29 -0
  265. package/dist/types/src/types/CreateSubscription.d.ts.map +1 -0
  266. package/dist/types/src/types/FeedOperation.d.ts +52 -0
  267. package/dist/types/src/types/FeedOperation.d.ts.map +1 -0
  268. package/dist/types/src/types/Magazine.d.ts +87 -0
  269. package/dist/types/src/types/Magazine.d.ts.map +1 -0
  270. package/dist/types/src/types/Magazine.test.d.ts +2 -0
  271. package/dist/types/src/types/Magazine.test.d.ts.map +1 -0
  272. package/dist/types/src/types/Subscription.d.ts +144 -0
  273. package/dist/types/src/types/Subscription.d.ts.map +1 -0
  274. package/dist/types/src/types/Subscription.test.d.ts +2 -0
  275. package/dist/types/src/types/Subscription.test.d.ts.map +1 -0
  276. package/dist/types/src/types/index.d.ts +5 -0
  277. package/dist/types/src/types/index.d.ts.map +1 -0
  278. package/dist/types/src/util/date.d.ts +8 -0
  279. package/dist/types/src/util/date.d.ts.map +1 -0
  280. package/dist/types/src/util/index.d.ts +4 -0
  281. package/dist/types/src/util/index.d.ts.map +1 -0
  282. package/dist/types/src/util/post-content.d.ts +14 -0
  283. package/dist/types/src/util/post-content.d.ts.map +1 -0
  284. package/dist/types/src/util/text.d.ts +18 -0
  285. package/dist/types/src/util/text.d.ts.map +1 -0
  286. package/dist/types/tsconfig.tsbuildinfo +1 -0
  287. package/dx.config.ts +48 -0
  288. package/package.json +164 -0
  289. package/src/MagazinePlugin.test.ts +36 -0
  290. package/src/MagazinePlugin.tsx +61 -0
  291. package/src/MagazinePlugin.workerd.ts +31 -0
  292. package/src/atoms/atoms.test.ts +323 -0
  293. package/src/atoms/index.ts +10 -0
  294. package/src/atoms/magazine-posts.ts +70 -0
  295. package/src/atoms/post-content.ts +34 -0
  296. package/src/atoms/post-curation.ts +36 -0
  297. package/src/atoms/post-display.ts +59 -0
  298. package/src/atoms/post-read.ts +40 -0
  299. package/src/atoms/post-tags.ts +66 -0
  300. package/src/capabilities/app-graph-builder.ts +125 -0
  301. package/src/capabilities/create-object.ts +127 -0
  302. package/src/capabilities/index.ts +17 -0
  303. package/src/capabilities/navigation-resolver.ts +21 -0
  304. package/src/capabilities/operation-handler.ts +16 -0
  305. package/src/capabilities/react-surface.tsx +53 -0
  306. package/src/capabilities/routine-templates.ts +16 -0
  307. package/src/capabilities/skill-definition.ts +14 -0
  308. package/src/components/PostContent/PostContent.test.ts +81 -0
  309. package/src/components/PostContent/PostContent.tsx +111 -0
  310. package/src/components/PostContent/index.ts +5 -0
  311. package/src/components/PostStack/PostStack.stories.tsx +39 -0
  312. package/src/components/PostStack/PostStack.tsx +151 -0
  313. package/src/components/PostStack/index.ts +5 -0
  314. package/src/components/SubscriptionStack/SubscriptionStack.stories.tsx +52 -0
  315. package/src/components/SubscriptionStack/SubscriptionStack.tsx +161 -0
  316. package/src/components/SubscriptionStack/index.ts +5 -0
  317. package/src/components/index.ts +7 -0
  318. package/src/containers/FeedArticle/FeedArticle.stories.tsx +102 -0
  319. package/src/containers/FeedArticle/FeedArticle.tsx +68 -0
  320. package/src/containers/FeedArticle/FeedToolbar.tsx +47 -0
  321. package/src/containers/FeedArticle/index.ts +5 -0
  322. package/src/containers/FeedProperties/FeedProperties.tsx +107 -0
  323. package/src/containers/FeedProperties/index.ts +5 -0
  324. package/src/containers/MagazineArticle/MagazineArticle.stories.tsx +244 -0
  325. package/src/containers/MagazineArticle/MagazineArticle.tsx +146 -0
  326. package/src/containers/MagazineArticle/MagazineTile.tsx +88 -0
  327. package/src/containers/MagazineArticle/index.ts +5 -0
  328. package/src/containers/MagazineArticle/useToolbar.tsx +145 -0
  329. package/src/containers/PostArticle/PostArticle.stories.tsx +117 -0
  330. package/src/containers/PostArticle/PostArticle.tsx +140 -0
  331. package/src/containers/PostArticle/PostToolbar.tsx +120 -0
  332. package/src/containers/PostArticle/index.ts +5 -0
  333. package/src/containers/PostCard/PostCard.tsx +70 -0
  334. package/src/containers/PostCard/index.ts +5 -0
  335. package/src/containers/SubscriptionsArticle/SubscriptionsArticle.stories.tsx +80 -0
  336. package/src/containers/SubscriptionsArticle/SubscriptionsArticle.tsx +99 -0
  337. package/src/containers/SubscriptionsArticle/index.ts +5 -0
  338. package/src/containers/index.ts +11 -0
  339. package/src/index.ts +6 -0
  340. package/src/meta.ts +9 -0
  341. package/src/operations/clear-magazine.ts +83 -0
  342. package/src/operations/curate-magazine.skill.conversations.json +1 -0
  343. package/src/operations/curate-magazine.skill.test.ts +167 -0
  344. package/src/operations/curate-magazine.test.ts +91 -0
  345. package/src/operations/curate-magazine.ts +178 -0
  346. package/src/operations/extraction/article.test.ts +154 -0
  347. package/src/operations/extraction/article.ts +270 -0
  348. package/src/operations/extraction/index.ts +5 -0
  349. package/src/operations/fetch-article-content.ts +27 -0
  350. package/src/operations/index.ts +13 -0
  351. package/src/operations/load-post-content.ts +76 -0
  352. package/src/operations/sources/article.ts +122 -0
  353. package/src/operations/sources/cors.ts +19 -0
  354. package/src/operations/sources/feed-fetcher.test.ts +200 -0
  355. package/src/operations/sources/feed-fetcher.ts +26 -0
  356. package/src/operations/sources/http.ts +42 -0
  357. package/src/operations/sources/index.ts +9 -0
  358. package/src/operations/sources/rss.test.ts +201 -0
  359. package/src/operations/sources/rss.ts +138 -0
  360. package/src/operations/sources/standard-site.ts +381 -0
  361. package/src/operations/sources/testing/feed.xml +477 -0
  362. package/src/operations/sync-feed.test.ts +146 -0
  363. package/src/operations/sync-feed.ts +140 -0
  364. package/src/operations/util.test.ts +67 -0
  365. package/src/operations/util.ts +65 -0
  366. package/src/paths.ts +14 -0
  367. package/src/plugin.ts +11 -0
  368. package/src/plugin.workerd.ts +6 -0
  369. package/src/skills/index.ts +5 -0
  370. package/src/skills/magazine-skill.ts +45 -0
  371. package/src/stories/ArticleExtractor.stories.tsx +204 -0
  372. package/src/stories/MagazineCurate.stories.tsx +217 -0
  373. package/src/stories/fixtures/theregister-ai.xml +45 -0
  374. package/src/stories/theregister-fixture.test.ts +35 -0
  375. package/src/templates/magazine-curation.ts +47 -0
  376. package/src/testing/builder.ts +77 -0
  377. package/src/testing/index.ts +5 -0
  378. package/src/translations.ts +81 -0
  379. package/src/types/CreateSubscription.ts +61 -0
  380. package/src/types/FeedOperation.ts +134 -0
  381. package/src/types/Magazine.test.ts +69 -0
  382. package/src/types/Magazine.ts +214 -0
  383. package/src/types/Subscription.test.ts +171 -0
  384. package/src/types/Subscription.ts +366 -0
  385. package/src/types/index.ts +8 -0
  386. package/src/util/date.ts +27 -0
  387. package/src/util/index.ts +7 -0
  388. package/src/util/post-content.ts +19 -0
  389. package/src/util/text.ts +98 -0
  390. package/src/vite-env.d.ts +10 -0
@@ -0,0 +1,270 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import Defuddle, { type DefuddleResponse } from 'defuddle/full';
6
+
7
+ /**
8
+ * Article extracted from a web page: main body content (denoised), plus
9
+ * structured metadata and any image URLs found on the page.
10
+ */
11
+ export type ExtractedArticle = {
12
+ /** Main body text rendered as Markdown. */
13
+ markdown: string;
14
+ /** Cleaned-up article HTML, suitable for rendering directly. */
15
+ html: string;
16
+ /** Article title (from <title>, og:title, schema.org, etc.). */
17
+ title?: string;
18
+ /** Byline / author. */
19
+ author?: string;
20
+ /** Site description / og:description. */
21
+ description?: string;
22
+ /** ISO 8601 published timestamp, when defuddle could find one. */
23
+ published?: string;
24
+ /** Lead image (og:image / twitter:image / first content image). */
25
+ image?: string;
26
+ /** Source domain (e.g. `example.com`). */
27
+ domain?: string;
28
+ /** All image URLs found in the extracted article, lead image first. */
29
+ imageUrls: string[];
30
+ /** Word count of the extracted body. */
31
+ wordCount?: number;
32
+ };
33
+
34
+ const isDocumentParserAvailable = (): boolean => typeof DOMParser !== 'undefined';
35
+
36
+ const collectImageUrls = (lead: string | undefined, contentHtml: string): string[] => {
37
+ const seen = new Set<string>();
38
+ const urls: string[] = [];
39
+ const push = (url: string | undefined) => {
40
+ if (!url || url.startsWith('data:') || seen.has(url)) {
41
+ return;
42
+ }
43
+ seen.add(url);
44
+ urls.push(url);
45
+ };
46
+ push(lead);
47
+ const imgRegex = /<img\b[^>]+src=["']([^"']+)["']/gi;
48
+ let match: RegExpExecArray | null;
49
+ while ((match = imgRegex.exec(contentHtml)) != null) {
50
+ push(match[1]);
51
+ }
52
+ return urls;
53
+ };
54
+
55
+ //
56
+ // Trailing-chrome pruning
57
+ //
58
+ // Defuddle's heuristics already strip nav/header/footer/aside at the page
59
+ // level, but on some sites tag clouds and "related" rails live INSIDE the
60
+ // `<article>`/`<main>` container — defuddle keeps them because they're
61
+ // structurally part of the body. We walk trailing children of the content
62
+ // root and drop anything that scores as chrome (tag-href list, link-only
63
+ // list, recognisable class/id, high link density). Only trailing siblings
64
+ // are touched — the same heading text mid-article is left alone.
65
+ //
66
+
67
+ /** Substring patterns on class/id that mark a block as chrome. */
68
+ const CHROME_CLASS_PATTERN =
69
+ /(?:^|[\s_-])(?:tag|topic|related|recirc|footer|sidebar|widget|share|social|comments?|disqus|recommend|more[-_]from|read[-_]next|outbrain|taboola)(?:[\s_-]|$)/i;
70
+
71
+ /** Hrefs that point to tag/category/related routes. */
72
+ const CHROME_HREF_PATTERN = /\/(?:tags?|topics?|categor(?:y|ies)|authors?|related|recommended)\//i;
73
+
74
+ const isHeading = (el: Element): boolean => /^H[1-6]$/i.test(el.tagName);
75
+
76
+ const linkDensity = (el: Element): number => {
77
+ const text = (el.textContent ?? '').trim();
78
+ if (text.length === 0) {
79
+ return 0;
80
+ }
81
+ const linkText = Array.from(el.querySelectorAll('a'))
82
+ .map((anchor) => (anchor.textContent ?? '').trim())
83
+ .join(' ');
84
+ return linkText.length / text.length;
85
+ };
86
+
87
+ /** A `<ul>` / `<ol>` whose items are essentially a single link each. */
88
+ const isLinkOnlyList = (el: Element): boolean => {
89
+ if (el.tagName !== 'UL' && el.tagName !== 'OL') {
90
+ return false;
91
+ }
92
+ const items = Array.from(el.children).filter((child) => child.tagName === 'LI');
93
+ if (items.length === 0) {
94
+ return false;
95
+ }
96
+ return items.every((li) => {
97
+ const links = li.querySelectorAll('a');
98
+ if (links.length !== 1) {
99
+ return false;
100
+ }
101
+ const text = (li.textContent ?? '').trim();
102
+ const linkText = (links[0].textContent ?? '').trim();
103
+ // The <li>'s text is essentially just the link text (allow a few stray chars).
104
+ return text.length > 0 && Math.abs(text.length - linkText.length) <= 4;
105
+ });
106
+ };
107
+
108
+ /** Block where the majority of links point to tag/category/related routes. */
109
+ const isTagURLBlock = (el: Element): boolean => {
110
+ const links = Array.from(el.querySelectorAll('a[href]'));
111
+ if (links.length < 2) {
112
+ return false;
113
+ }
114
+ const matching = links.filter((anchor) => CHROME_HREF_PATTERN.test(anchor.getAttribute('href') ?? ''));
115
+ return matching.length / links.length > 0.6;
116
+ };
117
+
118
+ /**
119
+ * True when the element has no body-content descendants. Body content is
120
+ * paragraphs, blockquotes, code/pre, figures, images, and tables — the
121
+ * signal that a block contributes article substance, not chrome. (Lists and
122
+ * raw `<a>` tags don't count: tag clouds and link rails are entirely those.)
123
+ */
124
+ const hasNoBodyContent = (el: Element): boolean =>
125
+ el.querySelector('p, blockquote, pre, code, figure, img, table') == null;
126
+
127
+ const isChromeElement = (el: Element): boolean => {
128
+ // Strong structural signals — fire alone.
129
+ const tag = el.tagName;
130
+ if (tag === 'ASIDE' || tag === 'NAV' || tag === 'FOOTER') {
131
+ return true;
132
+ }
133
+ const role = el.getAttribute('role');
134
+ if (role === 'navigation' || role === 'complementary' || role === 'contentinfo') {
135
+ return true;
136
+ }
137
+ // Link-only list is purely structural and self-contained — safe.
138
+ if (isLinkOnlyList(el)) {
139
+ return true;
140
+ }
141
+ // Tag-href ratio AND class-hint can both fire on a wrapper that *contains*
142
+ // a tag rail nested deep among real article paragraphs (the rail's links
143
+ // skew the container's link counts above threshold). Only treat as chrome
144
+ // when the element has no body-content descendants — i.e. it's structurally
145
+ // a link rail itself, not a body wrapper that happens to nest one.
146
+ if (!hasNoBodyContent(el)) {
147
+ return false;
148
+ }
149
+ if (isTagURLBlock(el)) {
150
+ return true;
151
+ }
152
+ const classNameAndId = `${(el.getAttribute('class') ?? '').toString()} ${el.id ?? ''}`;
153
+ if (CHROME_CLASS_PATTERN.test(classNameAndId)) {
154
+ return true;
155
+ }
156
+ return false;
157
+ };
158
+
159
+ /**
160
+ * Find the element defuddle is most likely to treat as the main content
161
+ * root, so we prune at the same scope it'll later extract. Mirrors the
162
+ * common selector chain article > main > [role=main] > body.
163
+ */
164
+ const findContentRoot = (doc: Document): Element | null =>
165
+ doc.querySelector('article') ?? doc.querySelector('main') ?? doc.querySelector('[role="main"]') ?? doc.body;
166
+
167
+ /**
168
+ * First pass: scan all descendants of the content root and remove elements
169
+ * that look like chrome wherever they appear. An element qualifies when:
170
+ * - its class/id contains a chrome keyword
171
+ * (`tag`, `topic`, `related`, `comments`, `share`, `widget`, `sidebar`,
172
+ * `footer`, `recommend`, etc.), AND
173
+ * - it has no body-content descendants (paragraphs, blockquotes, figures,
174
+ * tables) — i.e. it's a self-contained widget, not an article wrapper
175
+ * that happens to share a keyword.
176
+ * Plus elements that are structurally just a list of links (tag clouds,
177
+ * "related" rails) regardless of class.
178
+ *
179
+ * This catches chrome buried alongside body content in real-world layouts
180
+ * (e.g. theregister's `<div class="similar_topics">` and `<div class="comments">`
181
+ * nested as siblings of the article body inside `<div id="article-wrapper">`).
182
+ */
183
+ const pruneChromeDescendants = (root: Element): void => {
184
+ const candidates = Array.from(root.querySelectorAll('*'));
185
+ for (const el of candidates) {
186
+ // Skip elements already detached because an ancestor was removed.
187
+ if (!root.contains(el)) {
188
+ continue;
189
+ }
190
+ if (isChromeElement(el)) {
191
+ el.remove();
192
+ }
193
+ }
194
+ };
195
+
196
+ /**
197
+ * Second pass: when the descendant scan removed an element that used to sit
198
+ * at the trailing edge of the article, the heading that titled it is now
199
+ * dangling. Strip it.
200
+ *
201
+ * Naturally-trailing headings on legitimate articles (essays ending with
202
+ * `<h2>Conclusion</h2>`) are preserved because we only fire when the
203
+ * original trailing edge was actually severed.
204
+ */
205
+ const trimDanglingTrailingHeading = (root: Element): void => {
206
+ let cursor = root.lastElementChild;
207
+ while (cursor && isHeading(cursor) && cursor.nextElementSibling == null) {
208
+ const previous = cursor.previousElementSibling;
209
+ cursor.remove();
210
+ cursor = previous;
211
+ }
212
+ };
213
+
214
+ /** @internal exported for unit testing the chrome-pruning rules in isolation. */
215
+ export const pruneTrailingChrome = (doc: Document): void => {
216
+ const root = findContentRoot(doc);
217
+ if (!root) {
218
+ return;
219
+ }
220
+ // Capture the original trailing edge so we can tell after pruning whether
221
+ // any chrome was removed FROM the end (vs only from mid-article).
222
+ const originalLastChild = root.lastElementChild;
223
+ pruneChromeDescendants(root);
224
+ if (originalLastChild && !root.contains(originalLastChild)) {
225
+ trimDanglingTrailingHeading(root);
226
+ }
227
+ };
228
+
229
+ const mapResult = (result: DefuddleResponse): ExtractedArticle => {
230
+ const lead = result.image || undefined;
231
+ return {
232
+ markdown: result.contentMarkdown ?? '',
233
+ html: result.content ?? '',
234
+ title: result.title || undefined,
235
+ author: result.author || undefined,
236
+ description: result.description || undefined,
237
+ published: result.published || undefined,
238
+ image: lead,
239
+ domain: result.domain || undefined,
240
+ imageUrls: collectImageUrls(lead, result.content ?? ''),
241
+ wordCount: result.wordCount,
242
+ };
243
+ };
244
+
245
+ /**
246
+ * Extracts the main article from a web page's HTML using `defuddle`.
247
+ * Discards navigation, comments, ads, and other chrome; returns the body
248
+ * as Markdown plus structured metadata.
249
+ *
250
+ * Works in both browser/worker (uses `DOMParser`) and Node (delegates to
251
+ * `defuddle/node`, which uses linkedom). The Node path is loaded lazily so
252
+ * the linkedom dependency stays out of browser bundles.
253
+ */
254
+ export const extractArticle = async (html: string, url?: string): Promise<ExtractedArticle> => {
255
+ if (!html) {
256
+ return { markdown: '', html: '', imageUrls: [] };
257
+ }
258
+ if (isDocumentParserAvailable()) {
259
+ const doc = new DOMParser().parseFromString(html, 'text/html');
260
+ pruneTrailingChrome(doc);
261
+ return mapResult(new Defuddle(doc, { url, separateMarkdown: true }).parse());
262
+ }
263
+ // Node path: defuddle/node accepts an HTML string and returns a Promise.
264
+ // Trailing-chrome pruning is currently DOM-based and only runs in the
265
+ // browser path; agent operations using the Node path may see boilerplate
266
+ // bleed-through on sites like theregister.com until linkedom-based pruning
267
+ // is added.
268
+ const { Defuddle: DefuddleNode } = await import('defuddle/node');
269
+ return mapResult(await DefuddleNode(html, url, { separateMarkdown: true }));
270
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export * from './article';
@@ -0,0 +1,27 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+
7
+ import { Operation } from '@dxos/compute';
8
+ import { Database } from '@dxos/echo';
9
+ import { invariant } from '@dxos/invariant';
10
+
11
+ import { FeedOperation } from '../types';
12
+ import { browserCorsProxy, fetchArticle } from './sources';
13
+
14
+ const handler: Operation.WithHandler<typeof FeedOperation.FetchArticleContent> = FeedOperation.FetchArticleContent.pipe(
15
+ Operation.withHandler(
16
+ Effect.fn(function* ({ post: postRef }) {
17
+ const post = yield* Database.load(postRef);
18
+ invariant(post.link, 'Post has no link.');
19
+ return yield* Effect.tryPromise({
20
+ try: () => fetchArticle(post.link!, { corsProxy: browserCorsProxy() }),
21
+ catch: (error) => (error instanceof Error ? error : new Error(String(error))),
22
+ });
23
+ }),
24
+ ),
25
+ );
26
+
27
+ export default handler;
@@ -0,0 +1,13 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { OperationHandlerSet } from '@dxos/compute';
6
+
7
+ export const MagazineOperationHandlerSet = OperationHandlerSet.lazy(
8
+ () => import('./clear-magazine'),
9
+ () => import('./curate-magazine'),
10
+ () => import('./fetch-article-content'),
11
+ () => import('./load-post-content'),
12
+ () => import('./sync-feed'),
13
+ );
@@ -0,0 +1,76 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+
7
+ import { getSpace } from '@dxos/client/echo';
8
+ import { Operation } from '@dxos/compute';
9
+ import { invariant } from '@dxos/invariant';
10
+
11
+ import { FeedOperation, Subscription } from '../types';
12
+ import { makeSnippet, stripHtml } from '../util/text';
13
+ import { browserCorsProxy, fetchArticle } from './sources';
14
+
15
+ export default FeedOperation.LoadPostContent.pipe(
16
+ Operation.withHandler(
17
+ Effect.fn(function* ({ post: postRef, force }) {
18
+ // The Post is a queue item (its source Subscription lives in space.db).
19
+ // Resolve the source Subscription first; everything that needs a
20
+ // Database / Space binding comes from there. The Post's own
21
+ // `Obj.getDatabase` link isn't reliably set on every code path that
22
+ // produces a queue-decoded proxy, but the Subscription is always in
23
+ // space.db.
24
+ const post = postRef.target;
25
+ invariant(post, 'Post ref target not loaded.');
26
+ const subscription = post.source?.target;
27
+ if (!subscription || !post.link) {
28
+ return;
29
+ }
30
+ const space = getSpace(subscription);
31
+ invariant(space, 'Subscription is not in a space.');
32
+ // Idempotent by default (first-open auto-load); `force` re-fetches for the reader's refresh.
33
+ if (!force) {
34
+ const existing = yield* Effect.tryPromise(() => Subscription.findPostContent(subscription, post));
35
+ if (existing) {
36
+ return;
37
+ }
38
+ }
39
+
40
+ // Standard.site documents carry the full article body inline (`Post.content`), so the reader
41
+ // renders offline without a second network round-trip. Gated on the feed type: RSS keeps the
42
+ // HTTP fetch path because its `content:encoded` is frequently a truncated summary, not the full
43
+ // body.
44
+ const inlineContent = post.content;
45
+ if (subscription.type === 'standard-site' && inlineContent) {
46
+ yield* Effect.tryPromise(() =>
47
+ Subscription.appendPostContent(space, subscription, {
48
+ post,
49
+ text: inlineContent,
50
+ snippet: makeSnippet(stripHtml(inlineContent)),
51
+ }),
52
+ );
53
+ return;
54
+ }
55
+
56
+ yield* Effect.tryPromise({
57
+ try: async () => {
58
+ // In the browser, route through the dev-server CORS proxy. Server-side callers
59
+ // (e.g. agent operations) pass no proxy and fetch directly.
60
+ const { text, imageUrls } = await fetchArticle(post.link!, { corsProxy: browserCorsProxy() });
61
+ if (text) {
62
+ // Store the body plus refined snippet/imageUrl derived from the full article — preferred
63
+ // over the description-derived defaults wherever the Post is rendered.
64
+ await Subscription.appendPostContent(space, subscription, {
65
+ post,
66
+ text,
67
+ snippet: makeSnippet(stripHtml(text)),
68
+ imageUrl: imageUrls[0],
69
+ });
70
+ }
71
+ },
72
+ catch: (error) => (error instanceof Error ? error : new Error(String(error))),
73
+ });
74
+ }),
75
+ ),
76
+ );
@@ -0,0 +1,122 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { extractArticle } from '../extraction';
6
+ import { applyCorsProxy } from './cors';
7
+
8
+ const FETCH_TIMEOUT_MS = 10_000;
9
+ const MAX_RESPONSE_BYTES = 2_000_000;
10
+ const MAX_RESPONSE_BYTES_HEADER = 5_000_000;
11
+
12
+ /**
13
+ * Exact-match hostname denylist. Defense-in-depth against trivial SSRF when
14
+ * this runs in a trusted/worker context. Not a substitute for DNS-level
15
+ * egress filtering when available.
16
+ */
17
+ const BLOCKED_HOSTS = new Set([
18
+ 'localhost',
19
+ '127.0.0.1',
20
+ '0.0.0.0',
21
+ '::1',
22
+ '169.254.169.254', // AWS/GCP/Azure instance metadata endpoint.
23
+ 'metadata.google.internal',
24
+ ]);
25
+
26
+ /** Throws unless `link` is an http(s) URL targeting a non-loopback, non-metadata host. */
27
+ const validateUrl = (link: string): URL => {
28
+ const url = new URL(link);
29
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
30
+ throw new Error(`Unsupported protocol: ${url.protocol}`);
31
+ }
32
+ const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, '');
33
+ if (BLOCKED_HOSTS.has(host)) {
34
+ throw new Error(`Blocked host: ${host}`);
35
+ }
36
+ return url;
37
+ };
38
+
39
+ /**
40
+ * Read the body with a hard byte cap; prevents unbounded memory use on
41
+ * adversarial responses. Throws if the stream API is unavailable so we never
42
+ * silently bypass the cap.
43
+ */
44
+ const readCapped = async (response: Response, limit: number): Promise<string> => {
45
+ const reader = response.body?.getReader();
46
+ if (!reader) {
47
+ throw new Error('Response body stream unavailable.');
48
+ }
49
+ const decoder = new TextDecoder('utf-8');
50
+ let received = 0;
51
+ let out = '';
52
+ while (received < limit) {
53
+ const { value, done } = await reader.read();
54
+ if (done) {
55
+ break;
56
+ }
57
+ if (value) {
58
+ received += value.byteLength;
59
+ out += decoder.decode(value, { stream: true });
60
+ }
61
+ }
62
+ try {
63
+ await reader.cancel();
64
+ } catch {
65
+ // Ignore cancel errors.
66
+ }
67
+ return (out + decoder.decode()).slice(0, limit);
68
+ };
69
+
70
+ export type FetchArticleResult = {
71
+ /** Plain-text body extracted from the article HTML. */
72
+ text: string;
73
+ /** Image URLs found on the page (og:image first, then <img src=...>). */
74
+ imageUrls: string[];
75
+ };
76
+
77
+ export type FetchArticleOptions = {
78
+ /**
79
+ * URL prefix for a server-side fetch proxy used to bypass browser CORS.
80
+ * The target URL is appended (URL-encoded). E.g. `'/api/rss?url='`.
81
+ * When omitted, the article is fetched directly.
82
+ */
83
+ corsProxy?: string;
84
+ };
85
+
86
+ /**
87
+ * Fetches a post's article page over HTTP and returns extracted plain text
88
+ * plus any image URLs found. Applies protocol validation, a fetch timeout,
89
+ * a Content-Length rejection, and a streamed byte cap.
90
+ *
91
+ * Wraps the original error as `cause` so callers can distinguish
92
+ * AbortError (timeout) from network/fetch failures.
93
+ */
94
+ export const fetchArticle = async (link: string, options: FetchArticleOptions = {}): Promise<FetchArticleResult> => {
95
+ try {
96
+ const url = validateUrl(link);
97
+ const fetchTarget = applyCorsProxy(url.toString(), options.corsProxy);
98
+ const response = await fetch(fetchTarget, {
99
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
100
+ redirect: 'follow',
101
+ });
102
+
103
+ if (!response.ok) {
104
+ throw new Error(`Fetch failed: ${response.status} ${response.statusText}`);
105
+ }
106
+ const contentLength = response.headers.get('content-length');
107
+ if (contentLength && Number(contentLength) > MAX_RESPONSE_BYTES_HEADER) {
108
+ throw new Error(`Response too large: ${contentLength} bytes`);
109
+ }
110
+
111
+ const html = await readCapped(response, MAX_RESPONSE_BYTES);
112
+ const article = await extractArticle(html, url.toString());
113
+ return {
114
+ text: article.markdown,
115
+ imageUrls: article.imageUrls,
116
+ };
117
+ } catch (error) {
118
+ throw new Error(`Failed to fetch article: ${String(error)}`, {
119
+ cause: error instanceof Error ? error : undefined,
120
+ });
121
+ }
122
+ };
@@ -0,0 +1,19 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ /**
6
+ * Cross-origin fetch helpers. Feed/article URLs are arbitrary third-party origins, so in the browser
7
+ * they must be routed through the dev/edge RSS proxy; server-side (no `window`) they're fetched
8
+ * directly.
9
+ */
10
+
11
+ /** Browser proxy path; the target URL is appended URL-encoded. */
12
+ export const CORS_PROXY = '/api/rss?url=';
13
+
14
+ /** The proxy to use in the current environment: the browser proxy, or undefined server-side. */
15
+ export const browserCorsProxy = (): string | undefined => (typeof window !== 'undefined' ? CORS_PROXY : undefined);
16
+
17
+ /** Wraps `url` with `proxy` (URL-encoded) when a proxy is set, else returns `url` unchanged. */
18
+ export const applyCorsProxy = (url: string, proxy?: string): string =>
19
+ proxy ? `${proxy}${encodeURIComponent(url)}` : url;