@jant/core 0.6.16 → 0.7.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 (508) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +60 -4
  4. package/bin/commands/reset-password.js +21 -8
  5. package/bin/commands/site/snapshot/export.js +1 -1
  6. package/bin/commands/site/snapshot/import.js +1 -1
  7. package/bin/lib/d1-query.js +5 -16
  8. package/bin/lib/node-env.js +40 -4
  9. package/bin/lib/site-selection.js +85 -29
  10. package/bin/lib/wrangler-cli.js +28 -2
  11. package/bin/lib/wrangler-json.js +35 -0
  12. package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
  13. package/dist/client/.vite/manifest.json +255 -90
  14. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  15. package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
  16. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  17. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  18. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  19. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  20. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  21. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  22. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  23. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  24. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  25. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  26. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  27. package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
  28. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  29. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  30. package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
  31. package/dist/client/_assets/client-GiYENVw8.css +2 -0
  32. package/dist/client/_assets/client-VnFxJN7G.js +295 -0
  33. package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
  34. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  35. package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
  36. package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
  37. package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
  38. package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
  39. package/dist/index.js +2 -5
  40. package/dist/node.js +5 -7
  41. package/package.json +3 -3
  42. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  43. package/src/__tests__/basecoat-variants.test.ts +121 -0
  44. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  45. package/src/__tests__/export-hugo-build.test.ts +260 -0
  46. package/src/__tests__/export-service.test.ts +231 -0
  47. package/src/__tests__/helpers/component-css.ts +91 -0
  48. package/src/__tests__/helpers/db.ts +33 -0
  49. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  50. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  51. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  52. package/src/app.tsx +71 -16
  53. package/src/auth.ts +18 -1
  54. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  55. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  56. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  57. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  58. package/src/client/__tests__/compose-launch.test.ts +62 -1
  59. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  60. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  61. package/src/client/__tests__/copy-field.test.ts +162 -0
  62. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  63. package/src/client/__tests__/image-processor.test.ts +324 -2
  64. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  65. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  66. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  67. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  68. package/src/client/__tests__/toast.test.ts +23 -0
  69. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  70. package/src/client/__tests__/video-processor.test.ts +181 -0
  71. package/src/client/audio-processor.ts +24 -15
  72. package/src/client/audio-waveform.ts +49 -0
  73. package/src/client/collection-dialog-host.ts +72 -0
  74. package/src/client/collection-navigation.ts +51 -16
  75. package/src/client/collection-page-actions.ts +26 -0
  76. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  77. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  78. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  79. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  80. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  81. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  82. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  83. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  84. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  85. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  86. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  87. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  88. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  89. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  90. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  91. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  92. package/src/client/components/collection-dialog-types.ts +49 -0
  93. package/src/client/components/collection-manager-types.ts +32 -1
  94. package/src/client/components/collection-types.ts +1 -15
  95. package/src/client/components/compose-types.ts +75 -1
  96. package/src/client/components/jant-collection-dialog.ts +722 -0
  97. package/src/client/components/jant-collection-directory.ts +567 -182
  98. package/src/client/components/jant-collection-form.ts +53 -232
  99. package/src/client/components/jant-command-palette.ts +100 -49
  100. package/src/client/components/jant-compose-dialog.ts +1486 -707
  101. package/src/client/components/jant-compose-editor.ts +673 -336
  102. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  103. package/src/client/components/jant-config-editor.ts +65 -53
  104. package/src/client/components/jant-confirm-dialog.ts +13 -11
  105. package/src/client/components/jant-locale-picker.ts +418 -0
  106. package/src/client/components/jant-media-lightbox.ts +146 -136
  107. package/src/client/components/jant-nav-manager.ts +690 -366
  108. package/src/client/components/jant-post-menu.ts +1086 -282
  109. package/src/client/components/jant-post-picker.ts +324 -0
  110. package/src/client/components/jant-repo-picker.ts +138 -106
  111. package/src/client/components/jant-settings-avatar.ts +31 -27
  112. package/src/client/components/jant-settings-general.ts +617 -543
  113. package/src/client/components/jant-settings-language.ts +1087 -0
  114. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  115. package/src/client/components/jant-text-preview.ts +94 -86
  116. package/src/client/components/nav-manager-types.ts +62 -1
  117. package/src/client/components/settings-types.ts +14 -29
  118. package/src/client/components/smart-collection-conditions.ts +285 -0
  119. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  120. package/src/client/compose-bridge.ts +214 -141
  121. package/src/client/compose-launch.ts +76 -2
  122. package/src/client/compose-shortcuts.ts +6 -8
  123. package/src/client/compose-triggers.ts +50 -0
  124. package/src/client/copy-field.ts +108 -0
  125. package/src/client/custom-url-menu.ts +60 -4
  126. package/src/client/icons.ts +14 -0
  127. package/src/client/image-plan.ts +165 -0
  128. package/src/client/image-processor.ts +195 -117
  129. package/src/client/image-worker-client.ts +45 -0
  130. package/src/client/image-worker.ts +135 -0
  131. package/src/client/lazy-entries.ts +95 -0
  132. package/src/client/lazy-slugify.ts +2 -2
  133. package/src/client/media-scroll-hint.ts +43 -2
  134. package/src/client/mediabunny.ts +29 -0
  135. package/src/client/post-picker.ts +41 -0
  136. package/src/client/runtime-paths.ts +74 -1
  137. package/src/client/settings-bridge.ts +4 -10
  138. package/src/client/site-header-nav.js +18 -6
  139. package/src/client/smart-collection-dialog-host.ts +116 -0
  140. package/src/client/smart-collection-page-actions.ts +208 -0
  141. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  142. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  143. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  144. package/src/client/tiptap/bubble-menu.ts +47 -16
  145. package/src/client/tiptap/floating-position.ts +216 -2
  146. package/src/client/tiptap/inline-image-upload.ts +7 -3
  147. package/src/client/tiptap/link-toolbar.ts +22 -9
  148. package/src/client/toast.ts +13 -0
  149. package/src/client/types/vite-worker.d.ts +12 -0
  150. package/src/client/upload-with-metadata.ts +3 -13
  151. package/src/client/video-processor.ts +351 -107
  152. package/src/client-auth.ts +14 -31
  153. package/src/client-compose.ts +19 -0
  154. package/src/client-manage.ts +13 -0
  155. package/src/client-settings.ts +14 -0
  156. package/src/client-site.ts +10 -3
  157. package/src/client.ts +4 -0
  158. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  159. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  160. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  161. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  162. package/src/db/migrations/0032_married_tomas.sql +3 -0
  163. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  164. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  165. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  166. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  167. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  168. package/src/db/migrations/meta/_journal.json +21 -0
  169. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  170. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  171. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  172. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  173. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  174. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  175. package/src/db/migrations/pg/meta/_journal.json +21 -0
  176. package/src/db/pg/schema.ts +195 -27
  177. package/src/db/post-visibility.ts +117 -0
  178. package/src/db/schema.ts +222 -27
  179. package/src/i18n/__tests__/detect.test.ts +26 -81
  180. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  181. package/src/i18n/context.tsx +12 -2
  182. package/src/i18n/detect.ts +7 -66
  183. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  184. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  185. package/src/i18n/locales/public/en.po +490 -158
  186. package/src/i18n/locales/public/en.ts +1 -1
  187. package/src/i18n/locales/public/zh-Hans.po +487 -155
  188. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  189. package/src/i18n/locales/public/zh-Hant.po +487 -155
  190. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  191. package/src/i18n/locales/settings/en.po +551 -69
  192. package/src/i18n/locales/settings/en.ts +1 -1
  193. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  194. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  195. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  196. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  197. package/src/i18n/locales.ts +94 -0
  198. package/src/i18n/middleware.ts +12 -2
  199. package/src/i18n/supported-locales.ts +80 -0
  200. package/src/lib/__tests__/api-settings.test.ts +11 -2
  201. package/src/lib/__tests__/asset-path.test.ts +92 -0
  202. package/src/lib/__tests__/deferred.test.ts +90 -0
  203. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  204. package/src/lib/__tests__/discover.test.ts +410 -0
  205. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  206. package/src/lib/__tests__/feed.test.ts +1526 -15
  207. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  208. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  209. package/src/lib/__tests__/http-cache.test.ts +162 -0
  210. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  211. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  212. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  213. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  214. package/src/lib/__tests__/navigation.test.ts +135 -1
  215. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  216. package/src/lib/__tests__/schemas.test.ts +42 -4
  217. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  218. package/src/lib/__tests__/slug.test.ts +70 -0
  219. package/src/lib/__tests__/slugify.test.ts +85 -0
  220. package/src/lib/__tests__/storage.test.ts +32 -0
  221. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  222. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  223. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  224. package/src/lib/__tests__/url.test.ts +78 -72
  225. package/src/lib/__tests__/view-language.test.ts +413 -0
  226. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  227. package/src/lib/address.ts +57 -0
  228. package/src/lib/api-posts.ts +12 -0
  229. package/src/lib/asset-path.ts +98 -1
  230. package/src/lib/build-env.ts +16 -0
  231. package/src/lib/collection-groups.ts +5 -1
  232. package/src/lib/collection-paths.ts +3 -10
  233. package/src/lib/constants.ts +28 -3
  234. package/src/lib/copy-field.ts +36 -0
  235. package/src/lib/crypto.ts +40 -0
  236. package/src/lib/csp-builder.ts +2 -1
  237. package/src/lib/deferred.ts +79 -0
  238. package/src/lib/discover-ping.ts +141 -0
  239. package/src/lib/discover.ts +437 -0
  240. package/src/lib/env.ts +134 -0
  241. package/src/lib/errors.ts +33 -0
  242. package/src/lib/feed-policy.ts +236 -2
  243. package/src/lib/feed.ts +926 -73
  244. package/src/lib/filter-dimensions.ts +1349 -0
  245. package/src/lib/github-sync-site-config.ts +3 -0
  246. package/src/lib/hosted-signin.ts +1 -1
  247. package/src/lib/http-cache.ts +148 -0
  248. package/src/lib/hugo-markdown.ts +16 -0
  249. package/src/lib/icons.ts +9 -2
  250. package/src/lib/ids.ts +3 -0
  251. package/src/lib/jant-docs.ts +42 -0
  252. package/src/lib/lang-detect.ts +308 -0
  253. package/src/lib/media-helpers.ts +13 -1
  254. package/src/lib/navigation.ts +78 -11
  255. package/src/lib/pagination.ts +28 -24
  256. package/src/lib/per-language-surfaces.ts +67 -0
  257. package/src/lib/post-meta.ts +19 -0
  258. package/src/lib/render.tsx +55 -7
  259. package/src/lib/resolve-config.ts +53 -5
  260. package/src/lib/schemas.ts +201 -10
  261. package/src/lib/site-header-fragment.tsx +2 -1
  262. package/src/lib/site-unavailable-page.ts +47 -0
  263. package/src/lib/slug.ts +51 -7
  264. package/src/lib/slugify.ts +113 -0
  265. package/src/lib/startup-config.ts +4 -6
  266. package/src/lib/storage.ts +7 -0
  267. package/src/lib/summary.ts +50 -0
  268. package/src/lib/thread-fold.ts +161 -0
  269. package/src/lib/time.ts +32 -0
  270. package/src/lib/timeline.ts +42 -1
  271. package/src/lib/tiptap-render.ts +1 -2
  272. package/src/lib/upload.ts +16 -13
  273. package/src/lib/url.ts +139 -86
  274. package/src/lib/version.ts +10 -8
  275. package/src/lib/view-language.ts +436 -0
  276. package/src/lib/view.ts +51 -23
  277. package/src/lib/viewer-context.ts +62 -0
  278. package/src/middleware/__tests__/auth.test.ts +70 -2
  279. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  280. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  281. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  282. package/src/middleware/__tests__/session.test.ts +181 -33
  283. package/src/middleware/auth.ts +50 -31
  284. package/src/middleware/cache-control.ts +75 -10
  285. package/src/middleware/config.ts +6 -6
  286. package/src/middleware/error-handler.ts +12 -0
  287. package/src/middleware/onboarding.ts +32 -10
  288. package/src/middleware/secure-headers.ts +1 -1
  289. package/src/middleware/session.ts +96 -8
  290. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  291. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  292. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  293. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  294. package/src/node/__tests__/runtime.test.ts +37 -0
  295. package/src/node/index.ts +1 -0
  296. package/src/preset.css +76 -22
  297. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  298. package/src/routes/api/__tests__/collections.test.ts +114 -0
  299. package/src/routes/api/__tests__/discover.test.ts +222 -0
  300. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  301. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  302. package/src/routes/api/__tests__/settings.test.ts +24 -3
  303. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  304. package/src/routes/api/collections.ts +49 -6
  305. package/src/routes/api/custom-urls.ts +0 -1
  306. package/src/routes/api/discover.ts +95 -0
  307. package/src/routes/api/export.ts +3 -0
  308. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  309. package/src/routes/api/nav-items.ts +55 -8
  310. package/src/routes/api/posts.ts +159 -0
  311. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  312. package/src/routes/api/public/archive.ts +77 -81
  313. package/src/routes/api/public/posts.ts +23 -6
  314. package/src/routes/api/smart-collections.ts +132 -0
  315. package/src/routes/api/telegram.ts +2 -40
  316. package/src/routes/api/uploads.ts +1 -2
  317. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  318. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  319. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  320. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  321. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  322. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  323. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  324. package/src/routes/auth/setup.tsx +934 -186
  325. package/src/routes/auth/signin.tsx +10 -8
  326. package/src/routes/compose.tsx +5 -0
  327. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  328. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  329. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  330. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  331. package/src/routes/dash/custom-urls.tsx +1 -37
  332. package/src/routes/dash/settings.tsx +502 -41
  333. package/src/routes/discover-announce.ts +96 -0
  334. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  335. package/src/routes/feed/feed.ts +69 -40
  336. package/src/routes/feed/sitemap.ts +95 -11
  337. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  338. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  339. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  340. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  341. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  342. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  343. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  344. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  345. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  346. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  347. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  348. package/src/routes/pages/archive.tsx +471 -369
  349. package/src/routes/pages/collection.tsx +94 -96
  350. package/src/routes/pages/collections.tsx +38 -50
  351. package/src/routes/pages/featured.tsx +32 -13
  352. package/src/routes/pages/home.tsx +25 -6
  353. package/src/routes/pages/language.tsx +113 -0
  354. package/src/routes/pages/latest.tsx +26 -16
  355. package/src/routes/pages/new.tsx +3 -0
  356. package/src/routes/pages/page.tsx +178 -17
  357. package/src/routes/pages/partials.tsx +41 -0
  358. package/src/routes/pages/search.tsx +18 -3
  359. package/src/routes/pages/smart-collection.tsx +340 -0
  360. package/src/routes/pages/subscribe.tsx +182 -0
  361. package/src/runtime/__tests__/index.test.ts +19 -0
  362. package/src/runtime/__tests__/node.test.ts +129 -4
  363. package/src/runtime/cloudflare.ts +21 -2
  364. package/src/runtime/index.ts +27 -1
  365. package/src/runtime/node.ts +18 -1
  366. package/src/runtime/site.ts +95 -24
  367. package/src/services/__tests__/auth.test.ts +116 -7
  368. package/src/services/__tests__/collection.test.ts +108 -4
  369. package/src/services/__tests__/language.test.ts +495 -0
  370. package/src/services/__tests__/navigation.test.ts +213 -10
  371. package/src/services/__tests__/path.test.ts +180 -0
  372. package/src/services/__tests__/post-language.test.ts +830 -0
  373. package/src/services/__tests__/post.test.ts +46 -9
  374. package/src/services/__tests__/settings.test.ts +228 -17
  375. package/src/services/__tests__/site-admin.test.ts +5 -5
  376. package/src/services/__tests__/smart-collection.test.ts +651 -0
  377. package/src/services/about-page.ts +22 -1
  378. package/src/services/auth.ts +51 -49
  379. package/src/services/bootstrap.ts +188 -38
  380. package/src/services/collection-directory-position.ts +76 -0
  381. package/src/services/collection.ts +331 -46
  382. package/src/services/custom-url.ts +27 -22
  383. package/src/services/export-theme/assets/client-site.css +1 -1
  384. package/src/services/export-theme/assets/client-site.js +123 -123
  385. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  386. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  387. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  388. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  389. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  390. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  391. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  392. package/src/services/export-theme/styles/main.css +13 -1
  393. package/src/services/export.ts +278 -19
  394. package/src/services/index.ts +34 -3
  395. package/src/services/language.ts +412 -0
  396. package/src/services/mcp.ts +3 -1
  397. package/src/services/media.ts +14 -6
  398. package/src/services/navigation.ts +429 -67
  399. package/src/services/path.ts +221 -6
  400. package/src/services/post.ts +1021 -152
  401. package/src/services/search.ts +39 -2
  402. package/src/services/settings.ts +238 -43
  403. package/src/services/site-admin.ts +24 -7
  404. package/src/services/site.ts +77 -1
  405. package/src/services/smart-collection.ts +636 -0
  406. package/src/services/telegram.ts +20 -22
  407. package/src/services/upload-session.ts +1 -3
  408. package/src/style-author.css +27 -0
  409. package/src/styles/components-author.css +1622 -0
  410. package/src/styles/components.css +24 -1571
  411. package/src/styles/site-media.css +16 -2
  412. package/src/styles/tokens.css +82 -12
  413. package/src/styles/ui-author.css +7602 -0
  414. package/src/styles/ui.css +1087 -7219
  415. package/src/types/app-context.ts +7 -0
  416. package/src/types/bindings.ts +3 -0
  417. package/src/types/config.ts +94 -15
  418. package/src/types/constants.ts +188 -16
  419. package/src/types/entities.ts +87 -1
  420. package/src/types/operations.ts +64 -2
  421. package/src/types/props.ts +157 -31
  422. package/src/types/views.ts +55 -1
  423. package/src/ui/__tests__/font-themes.test.ts +52 -12
  424. package/src/ui/compose/ComposeDialog.tsx +201 -7
  425. package/src/ui/compose/ComposePrompt.tsx +1 -5
  426. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  427. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  428. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  429. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  430. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  431. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  432. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  433. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  434. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  435. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  436. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  437. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  438. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  439. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  440. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  441. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  442. package/src/ui/dash/settings/discover-copy.ts +116 -0
  443. package/src/ui/feed/LinkCard.tsx +55 -45
  444. package/src/ui/feed/NoteCard.tsx +9 -4
  445. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  446. package/src/ui/feed/QuoteCard.tsx +55 -30
  447. package/src/ui/feed/ThreadPreview.tsx +6 -2
  448. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  449. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  450. package/src/ui/feed/thread-preview-state.ts +20 -6
  451. package/src/ui/font-themes.ts +39 -29
  452. package/src/ui/layouts/BaseLayout.tsx +155 -10
  453. package/src/ui/layouts/SiteLayout.tsx +192 -34
  454. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  455. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  456. package/src/ui/pages/ArchivePage.tsx +282 -289
  457. package/src/ui/pages/CollectionPage.tsx +86 -76
  458. package/src/ui/pages/CollectionsPage.tsx +5 -0
  459. package/src/ui/pages/ComposePage.tsx +8 -0
  460. package/src/ui/pages/PostPage.tsx +47 -0
  461. package/src/ui/pages/SearchPage.tsx +3 -3
  462. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  463. package/src/ui/pages/SubscribePage.tsx +236 -0
  464. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  465. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  466. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  467. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  468. package/src/ui/shared/CollectionsManager.tsx +70 -17
  469. package/src/ui/shared/CopyField.tsx +127 -0
  470. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  471. package/src/ui/shared/MediaGallery.tsx +125 -46
  472. package/src/ui/shared/Pagination.tsx +20 -5
  473. package/src/ui/shared/PostFooter.tsx +20 -11
  474. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  475. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  476. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  477. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  478. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  479. package/src/ui/shared/archive-labels.ts +98 -0
  480. package/src/ui/shared/collection-management-labels.ts +94 -16
  481. package/src/ui/shared/navigation-labels.ts +45 -71
  482. package/src/ui/shared/post-article-attributes.ts +2 -0
  483. package/src/ui/shared/smart-collection-labels.ts +343 -0
  484. package/dist/app-C75Zc8dD.js +0 -6
  485. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  486. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  487. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  488. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  489. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  490. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  491. package/dist/env-BPYViDJJ.js +0 -281
  492. package/dist/github-api-BNF35Lkx.js +0 -176
  493. package/dist/github-app-DdiD-bC4.js +0 -275
  494. package/dist/github-sync-Bqg62IvV.js +0 -4
  495. package/dist/github-sync-HdK2EgvJ.js +0 -431
  496. package/dist/url-CbLAtlZe.js +0 -900
  497. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  498. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  499. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  500. package/src/client/collection-created-notice.ts +0 -66
  501. package/src/client/collection-form-bridge.ts +0 -191
  502. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  503. package/src/client/components/jant-post-form.ts +0 -422
  504. package/src/client/components/post-form-template.ts +0 -352
  505. package/src/client/components/post-form-types.ts +0 -96
  506. package/src/client/media-metadata.ts +0 -247
  507. package/src/client/post-form-bridge.ts +0 -193
  508. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -2,7 +2,9 @@
2
2
  * Static asset path helpers.
3
3
  *
4
4
  * Build outputs live under an internal `/_assets` directory, while the public
5
- * asset base path may be prefixed by the site's deployment path.
5
+ * asset base path may be prefixed by the site's deployment path, or replaced
6
+ * by an absolute CDN URL. The connection hints a page needs are derived from
7
+ * those same base URLs, so they live here too.
6
8
  */
7
9
 
8
10
  import { toPublicPath } from "./url.js";
@@ -67,6 +69,101 @@ export function isAssetPath(path: string, basePath = ASSET_BASE_PATH): boolean {
67
69
  );
68
70
  }
69
71
 
72
+ /** One `<link rel="preconnect">` the layout should emit. */
73
+ export interface PreconnectHint {
74
+ /** Origin to open a connection to. */
75
+ href: string;
76
+ /**
77
+ * Whether this is the CORS-mode connection. A browser pools credentialed
78
+ * and uncredentialed connections separately, so an origin serving both a
79
+ * stylesheet (no CORS) and a module script (CORS) needs one of each.
80
+ */
81
+ crossorigin: boolean;
82
+ }
83
+
84
+ /**
85
+ * The origin of `base`, or `null` when it is not a cross-origin absolute URL.
86
+ */
87
+ function crossOrigin(base: string | undefined, siteOrigin: string | null) {
88
+ const trimmed = base?.trim();
89
+ if (
90
+ !trimmed ||
91
+ (!trimmed.startsWith("http://") && !trimmed.startsWith("https://"))
92
+ ) {
93
+ return null;
94
+ }
95
+ try {
96
+ const { origin } = new URL(trimmed);
97
+ return origin === siteOrigin ? null : origin;
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Origins worth a `<link rel="preconnect">` for the subresources of a page.
105
+ *
106
+ * Only a host the page does not already have a connection to earns a hint:
107
+ * the site's own origin is connected by the time the HTML arrives, and a
108
+ * same-origin base path resolves to it. What is left is a CDN asset host and
109
+ * a media host, and warming DNS, TCP and TLS while the rest of `<head>` parses
110
+ * takes that round trip off the critical path.
111
+ *
112
+ * The asset host serves the render-blocking stylesheet and the module script,
113
+ * so it gets both connection kinds; the media host serves images, so it gets
114
+ * the uncredentialed one. When the two are the same host, the asset hints
115
+ * already cover the images.
116
+ *
117
+ * @param options - Resolved base URLs for the page's subresources
118
+ * @param options.assetBasePath - Public asset base path or absolute CDN URL
119
+ * @param options.mediaBaseUrl - Public media base URL, empty when same-origin
120
+ * @param options.siteUrl - The site's own URL, so its origin is never hinted
121
+ * @returns Hints in the order they should be emitted, possibly empty
122
+ *
123
+ * @example
124
+ * ```ts
125
+ * getPreconnectHints({
126
+ * assetBasePath: "https://cdn.example.com/a",
127
+ * mediaBaseUrl: "https://media.example.com",
128
+ * siteUrl: "https://example.com",
129
+ * });
130
+ * // [
131
+ * // { href: "https://cdn.example.com", crossorigin: false },
132
+ * // { href: "https://cdn.example.com", crossorigin: true },
133
+ * // { href: "https://media.example.com", crossorigin: false },
134
+ * // ]
135
+ * ```
136
+ */
137
+ export function getPreconnectHints(options: {
138
+ assetBasePath: string;
139
+ mediaBaseUrl?: string;
140
+ siteUrl?: string;
141
+ }): PreconnectHint[] {
142
+ let siteOrigin: string | null = null;
143
+ if (options.siteUrl) {
144
+ try {
145
+ siteOrigin = new URL(options.siteUrl).origin;
146
+ } catch {
147
+ // An unparseable siteUrl tells us nothing; hint the other hosts anyway.
148
+ }
149
+ }
150
+
151
+ const assetOrigin = crossOrigin(options.assetBasePath, siteOrigin);
152
+ const mediaOrigin = crossOrigin(options.mediaBaseUrl, siteOrigin);
153
+
154
+ const hints: PreconnectHint[] = [];
155
+ if (assetOrigin) {
156
+ hints.push(
157
+ { href: assetOrigin, crossorigin: false },
158
+ { href: assetOrigin, crossorigin: true },
159
+ );
160
+ }
161
+ if (mediaOrigin && mediaOrigin !== assetOrigin) {
162
+ hints.push({ href: mediaOrigin, crossorigin: false });
163
+ }
164
+ return hints;
165
+ }
166
+
70
167
  /**
71
168
  * Convert an internal asset path into its public deployment path.
72
169
  *
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Build-time environment detection
3
+ *
4
+ * In Vite dev, `__JANT_DEV__` is replaced with `true` via Vite's `define`
5
+ * config. In production (wrangler/esbuild) the typeof check evaluates to false
6
+ * safely.
7
+ *
8
+ * Kept apart from `version.ts` so client bundles can read the flag: the client
9
+ * build defines `__JANT_VERSION__` but none of the Worker-only asset globals
10
+ * that `version.ts` reads at module scope.
11
+ */
12
+
13
+ declare const __JANT_DEV__: boolean | undefined;
14
+
15
+ export const IS_VITE_DEV =
16
+ typeof __JANT_DEV__ !== "undefined" && __JANT_DEV__ === true;
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  export interface GroupableCollectionItem {
6
- type: "collection" | "divider" | "link";
6
+ type: "collection" | "smart_collection" | "divider" | "link";
7
7
  label?: string | null;
8
8
  url?: string | null;
9
9
  collection?: {
@@ -23,6 +23,10 @@ export interface DividerCollectionGroup {
23
23
  * next divider. Groups with fewer than two collections do not produce an
24
24
  * aggregate selection.
25
25
  *
26
+ * A smart collection sitting in the group contributes nothing and does not
27
+ * break it: `/collections/{a+b}` is a union of tagged sets, and a set defined
28
+ * by conditions has no slug to union in. It is skipped the same way a link is.
29
+ *
26
30
  * @param items - Ordered collection directory items
27
31
  * @param dividerIndex - Index of the divider item to inspect
28
32
  * @returns Aggregate slug expression and count, or `null`
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Collection URL helpers.
3
3
  *
4
- * Single collections use root-level paths. Collection directories, editors,
5
- * and aggregate views live under `/collections`.
4
+ * Single collections use root-level paths. The collections directory and
5
+ * aggregate views live under `/collections`. There is no editor path: a
6
+ * collection is created and edited in a dialog.
6
7
  */
7
8
 
8
9
  export const COLLECTIONS_DIRECTORY_PATH = "/collections";
@@ -30,11 +31,3 @@ export function getCollectionSelectionPath(slugExpression: string): string {
30
31
  export function getCollectionSelectionFeedPath(slugExpression: string): string {
31
32
  return `${getCollectionSelectionPath(slugExpression)}/feed`;
32
33
  }
33
-
34
- export function getNewCollectionPath(): string {
35
- return `${COLLECTIONS_DIRECTORY_PATH}/new`;
36
- }
37
-
38
- export function getCollectionEditPath(slug: string): string {
39
- return `${COLLECTIONS_DIRECTORY_PATH}/${slug}/edit`;
40
- }
@@ -16,6 +16,7 @@ export const RESERVED_PATHS = [
16
16
  "api",
17
17
  "feed",
18
18
  "search",
19
+ "subscribe",
19
20
  "archive",
20
21
  "media",
21
22
  "pages",
@@ -36,10 +37,27 @@ export type ReservedPath = (typeof RESERVED_PATHS)[number];
36
37
 
37
38
  /**
38
39
  * Check if a path is reserved
40
+ *
41
+ * @param path - Stored path form (no leading slash)
42
+ * @param languagePrefixes - URL prefixes of the site's additional languages
43
+ * (lowercase tags). While those are live, `/{prefix}` and everything under it
44
+ * is served by the language views, so no slug or custom URL may claim them.
45
+ * Passed per call rather than baked into the static list because the set is
46
+ * per-site and changes at runtime.
47
+ * @returns Whether the path's first segment is unavailable
48
+ * @example
49
+ * isReservedPath("archive"); // true
50
+ * isReservedPath("ja/hello", ["ja"]); // true
51
+ * isReservedPath("ja/hello"); // false — no language configured
39
52
  */
40
- export function isReservedPath(path: string): boolean {
53
+ export function isReservedPath(
54
+ path: string,
55
+ languagePrefixes: readonly string[] = [],
56
+ ): boolean {
41
57
  const firstSegment = path.split("/")[0]?.toLowerCase();
42
- return RESERVED_PATHS.includes(firstSegment as ReservedPath);
58
+ if (!firstSegment) return false;
59
+ if (RESERVED_PATHS.includes(firstSegment as ReservedPath)) return true;
60
+ return languagePrefixes.includes(firstSegment);
43
61
  }
44
62
 
45
63
  /**
@@ -66,10 +84,17 @@ export const SETTINGS_KEYS = Object.fromEntries(
66
84
  export type SettingsKey = SettingsFieldKey;
67
85
 
68
86
  /**
69
- * Onboarding status values
87
+ * Onboarding status values.
88
+ *
89
+ * `provisioned` is the hosted middle state: a control plane created the site
90
+ * and its owner, so the site is real and servable, but nobody has yet answered
91
+ * the questions only a person can answer — the language they write in. It is
92
+ * deliberately distinct from `pending`, which means the site has no owner at
93
+ * all and nothing but setup should be reachable.
70
94
  */
71
95
  export const ONBOARDING_STATUS = {
72
96
  PENDING: "pending",
97
+ PROVISIONED: "provisioned",
73
98
  COMPLETED: "completed",
74
99
  } as const;
75
100
 
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Copy field contract.
3
+ *
4
+ * A copy field is a read-only text input holding an address, with a button that
5
+ * puts it on the clipboard. It is rendered from two places — server JSX on the
6
+ * public subscribe page, a Lit template in General settings — so the hooks the
7
+ * client enhancer looks for and the classes that give the two the same shape
8
+ * are declared once, here, rather than written out twice.
9
+ *
10
+ * The behavior lives in `client/copy-field.ts`.
11
+ */
12
+
13
+ /** Marks the button. Its value is the toast shown after a successful copy. */
14
+ export const COPY_FIELD_BUTTON_ATTR = "data-copy-field";
15
+
16
+ /** On the button. Its value is the toast shown when the clipboard refuses. */
17
+ export const COPY_FIELD_FAILED_ATTR = "data-copy-field-failed";
18
+
19
+ /** Marks the input holding the address to copy. */
20
+ export const COPY_FIELD_VALUE_ATTR = "data-copy-field-value";
21
+
22
+ /**
23
+ * The address input. Monospaced because these are URLs read character by
24
+ * character, and right-padded to clear the button sitting on top of it.
25
+ */
26
+ export const COPY_FIELD_INPUT_CLASS = "input w-full pr-20 font-mono text-sm";
27
+
28
+ /** The button, laid over the input's right edge. */
29
+ export const COPY_FIELD_BUTTON_CLASS =
30
+ "btn-sm-outline absolute right-2 top-1/2 h-7 -translate-y-1/2 px-2.5 text-xs";
31
+
32
+ /** Wraps input and button so the button can be positioned against the input. */
33
+ export const COPY_FIELD_CONTROL_CLASS = "relative";
34
+
35
+ /** Wraps the whole field: label, optional description, then the control. */
36
+ export const COPY_FIELD_CLASS = "flex min-w-0 flex-col gap-1";
package/src/lib/crypto.ts CHANGED
@@ -45,3 +45,43 @@ export function timingSafeEqualText(a: string, b: string): boolean {
45
45
  const encoder = new TextEncoder();
46
46
  return timingSafeEqualBytes(encoder.encode(a), encoder.encode(b));
47
47
  }
48
+
49
+ /**
50
+ * Derive a hex HMAC-SHA256 of `message` under `secret`.
51
+ *
52
+ * Deriving a value instead of storing one is what lets a token be handed out
53
+ * repeatedly without a server-side record: the same inputs always produce the
54
+ * same output, so re-issuing is free and issuing twice cannot invalidate the
55
+ * first copy.
56
+ *
57
+ * @param secret - Key material, never leaves the server
58
+ * @param message - Value being signed
59
+ * @returns Lowercase hex signature, 64 characters
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * const signature = await hmacHex(env.AUTH_SECRET, `delete-account:${sessionId}`);
64
+ * ```
65
+ */
66
+ export async function hmacHex(
67
+ secret: string,
68
+ message: string,
69
+ ): Promise<string> {
70
+ const encoder = new TextEncoder();
71
+ const key = await crypto.subtle.importKey(
72
+ "raw",
73
+ encoder.encode(secret),
74
+ { name: "HMAC", hash: "SHA-256" },
75
+ false,
76
+ ["sign"],
77
+ );
78
+ const signature = await crypto.subtle.sign(
79
+ "HMAC",
80
+ key,
81
+ encoder.encode(message),
82
+ );
83
+
84
+ return Array.from(new Uint8Array(signature))
85
+ .map((byte) => byte.toString(16).padStart(2, "0"))
86
+ .join("");
87
+ }
@@ -71,7 +71,8 @@ export function buildCspDirectives(
71
71
  } = input;
72
72
 
73
73
  // Base script-src: same-origin, Datastar's `unsafe-eval` for data-on-* /
74
- // data-signals expressions, blob: for media workers (heic-to, mediabunny).
74
+ // data-signals expressions, blob: for media workers (heic-to, mediabunny,
75
+ // the inline image worker).
75
76
  const scriptSrc = ["'self'", "'unsafe-eval'", "blob:"];
76
77
  if (allowInlineScript) scriptSrc.push("'unsafe-inline'");
77
78
  appendUnique(scriptSrc, assetOrigin);
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Background work that outlives the HTTP response that started it.
3
+ *
4
+ * Some work must not be waited on: a Telegram webhook has to ACK before the
5
+ * album it triggered is assembled, or Telegram serializes the next update in
6
+ * that chat behind it; a Discover announcement has to leave the settings save
7
+ * alone, because a directory being down is not a reason a save fails. Both
8
+ * want the same thing — start it, answer the request, let it finish.
9
+ *
10
+ * The shape is small but every part of it is load-bearing, which is why it is
11
+ * one function rather than four lines copied per call site:
12
+ *
13
+ * - **The promise carries its own handler.** Nothing is awaiting it, so a
14
+ * rejection has nobody to reject to. Left unhandled, Node ends the process.
15
+ * - **`waitUntil` is optional and its accessor throws.** On Workers it keeps
16
+ * the isolate alive until the promise settles. Under Node and in tests
17
+ * Hono's `c.executionCtx` getter throws rather than returning undefined, so
18
+ * reaching for it needs a `try`, and the promise is simply left to the event
19
+ * loop.
20
+ * - **Tests need a handle on it.** With no `waitUntil` there is nothing to
21
+ * await, so a test that has to assert on the result registers an array at
22
+ * `__pendingDeferred` on the env and gets every promise pushed into it.
23
+ */
24
+
25
+ /**
26
+ * An env binding carrying the test hook.
27
+ *
28
+ * Registered by a test that needs to `await` deferred work before asserting —
29
+ * production bindings never have it, and the array is simply not found.
30
+ */
31
+ export interface DeferredPendingEnv {
32
+ __pendingDeferred?: Promise<unknown>[];
33
+ }
34
+
35
+ /**
36
+ * Start work that must finish after the response has been sent.
37
+ *
38
+ * Never throws and never rejects: a failure is logged under `label` and goes
39
+ * no further, because there is no caller left to tell.
40
+ *
41
+ * @param c - Request context, for the env binding and the runtime's
42
+ * background-work hook. A full Hono `Context` satisfies this.
43
+ * @param label - What this work is, for the log line when it fails. A short
44
+ * subject in the sentence "`<label>` background error" — "Telegram",
45
+ * "Discover announcement".
46
+ * @param work - The work itself. Its rejection is handled here.
47
+ * @returns Nothing — the point is that the caller does not wait.
48
+ * @example
49
+ * ```ts
50
+ * runDeferred(c, "Discover announcement", async () => {
51
+ * await services.settings.announceToDiscover({ endpoint, feedUrl });
52
+ * });
53
+ * ```
54
+ */
55
+ export function runDeferred(
56
+ c: {
57
+ env: object;
58
+ executionCtx?: { waitUntil: (promise: Promise<unknown>) => void };
59
+ },
60
+ label: string,
61
+ work: () => Promise<void>,
62
+ ): void {
63
+ const promise = work().catch((error: unknown) => {
64
+ const message = error instanceof Error ? error.message : String(error);
65
+ // eslint-disable-next-line no-console -- Background failures must be visible.
66
+ console.error(`[Jant] ${label} background error: ${message}`);
67
+ });
68
+
69
+ try {
70
+ c.executionCtx?.waitUntil(promise);
71
+ } catch {
72
+ // executionCtx not available (e.g. Node, tests) — the promise still runs.
73
+ }
74
+
75
+ const pending = (c.env as DeferredPendingEnv).__pendingDeferred;
76
+ if (Array.isArray(pending)) {
77
+ pending.push(promise);
78
+ }
79
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Announcing a site to a Jant Discover directory.
3
+ *
4
+ * A directory cannot list a self-hosted site it has never heard of, and a site
5
+ * that publishes quietly has no reason to be found. The ping closes that gap
6
+ * with the smallest thing that works: one request, carrying one URL, sent only
7
+ * when someone deliberately turns the feature on.
8
+ *
9
+ * It is not a heartbeat and not a publish notification. It never fires
10
+ * periodically, never per post, and never while the setting is off. Whether a
11
+ * site is listed is still decided by what its own feed says, which the
12
+ * directory has to fetch and read for itself.
13
+ *
14
+ * This module is transport only. It reports what happened; recording it is the
15
+ * settings service's job, because an owner who cannot see whether the
16
+ * announcement got through has no way to tell "waiting" from "lost".
17
+ */
18
+
19
+ /** How long to wait before giving up on one attempt. */
20
+ const PING_TIMEOUT_MS = 5_000;
21
+
22
+ /**
23
+ * Attempts one announcement makes before it is left to the owner.
24
+ *
25
+ * Two, because the failure worth retrying automatically is the directory being
26
+ * mid-deploy or answering a transient 5xx. Anything that survives a second
27
+ * attempt — a blocked egress, a proxy eating the request, a directory that
28
+ * moved — is not going to be fixed by a third, and the settings page offers a
29
+ * Retry the owner can press when they have fixed it.
30
+ */
31
+ const PING_ATTEMPTS = 2;
32
+
33
+ /** Wait between the two attempts. */
34
+ const PING_RETRY_DELAY_MS = 2_000;
35
+
36
+ /** What one announcement attempt came to. */
37
+ export interface DiscoverAnnounceOutcome {
38
+ /** Unix seconds when the attempt finished. */
39
+ at: number;
40
+ /** The directory acknowledged it. */
41
+ ok: boolean;
42
+ /** The feed address that was sent. */
43
+ feedUrl: string;
44
+ /** HTTP status, when there was a response at all. */
45
+ status?: number;
46
+ /** Why it failed, for the owner to read. Absent on success. */
47
+ error?: string;
48
+ }
49
+
50
+ /**
51
+ * Tell the configured directory where this site's feed is.
52
+ *
53
+ * Resolves rather than throws: the caller runs this as background work behind
54
+ * a settings save, which must not fail because a directory is down.
55
+ *
56
+ * @param input - The directory endpoint, the feed to announce, and the clock
57
+ * @returns What the attempt came to, success or not
58
+ * @example
59
+ * ```ts
60
+ * const outcome = await sendDiscoverPing({
61
+ * endpoint: "https://jant.me/api/discover/ping",
62
+ * feedUrl: "https://example.com/latest/feed",
63
+ * now: () => Math.floor(Date.now() / 1000),
64
+ * });
65
+ * ```
66
+ */
67
+ export async function sendDiscoverPing(input: {
68
+ endpoint: string;
69
+ feedUrl: string;
70
+ now: () => number;
71
+ /** Injectable so tests do not sit through the retry delay. */
72
+ sleep?: (ms: number) => Promise<void>;
73
+ }): Promise<DiscoverAnnounceOutcome> {
74
+ const sleep =
75
+ input.sleep ??
76
+ ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
77
+
78
+ let last: { status?: number; error: string } = {
79
+ error: "The announcement was not attempted.",
80
+ };
81
+
82
+ for (let attempt = 1; attempt <= PING_ATTEMPTS; attempt += 1) {
83
+ const result = await attemptPing(input.endpoint, input.feedUrl);
84
+ if (result.ok) {
85
+ return {
86
+ at: input.now(),
87
+ ok: true,
88
+ feedUrl: input.feedUrl,
89
+ status: result.status,
90
+ };
91
+ }
92
+ last = {
93
+ error: result.error,
94
+ ...(result.status ? { status: result.status } : {}),
95
+ };
96
+ if (attempt < PING_ATTEMPTS) await sleep(PING_RETRY_DELAY_MS);
97
+ }
98
+
99
+ return {
100
+ at: input.now(),
101
+ ok: false,
102
+ feedUrl: input.feedUrl,
103
+ ...(last.status === undefined ? {} : { status: last.status }),
104
+ error: last.error,
105
+ };
106
+ }
107
+
108
+ async function attemptPing(
109
+ endpoint: string,
110
+ feedUrl: string,
111
+ ): Promise<
112
+ { ok: true; status: number } | { ok: false; status?: number; error: string }
113
+ > {
114
+ const controller = new AbortController();
115
+ const timer = setTimeout(() => controller.abort(), PING_TIMEOUT_MS);
116
+
117
+ try {
118
+ // The feed URL and nothing else. No identity, no version, no counts: a
119
+ // directory needs an address to fetch, and anything more would make this
120
+ // telemetry.
121
+ const response = await fetch(endpoint, {
122
+ method: "POST",
123
+ headers: { "Content-Type": "application/json" },
124
+ body: JSON.stringify({ feed: feedUrl }),
125
+ signal: controller.signal,
126
+ });
127
+ if (response.ok) return { ok: true, status: response.status };
128
+ return {
129
+ ok: false,
130
+ status: response.status,
131
+ error: `The directory answered ${response.status}.`,
132
+ };
133
+ } catch (error) {
134
+ return {
135
+ ok: false,
136
+ error: error instanceof Error ? error.message : String(error),
137
+ };
138
+ } finally {
139
+ clearTimeout(timer);
140
+ }
141
+ }