@jant/core 0.6.16 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (592) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/export.js +3 -1
  4. package/bin/commands/import-site.js +749 -220
  5. package/bin/commands/reset-password.js +21 -8
  6. package/bin/commands/setup.js +136 -0
  7. package/bin/commands/site/export.js +71 -34
  8. package/bin/commands/site/pull-media.js +2 -6
  9. package/bin/commands/site/snapshot/export.js +19 -61
  10. package/bin/commands/site/snapshot/import.js +23 -24
  11. package/bin/lib/d1-query.js +92 -18
  12. package/bin/lib/hugo-markdown.js +4 -0
  13. package/bin/lib/node-env.js +40 -4
  14. package/bin/lib/site-pull-media.js +21 -28
  15. package/bin/lib/site-selection.js +94 -29
  16. package/bin/lib/site-snapshot.js +338 -3
  17. package/bin/lib/sql-export.js +68 -5
  18. package/bin/lib/wrangler-cli.js +37 -2
  19. package/bin/lib/wrangler-json.js +35 -0
  20. package/bin/lib/zip-archive.js +187 -0
  21. package/dist/{app-C7_4oab9.js → app-ZcaI1kPN.js} +53190 -13529
  22. package/dist/client/.vite/manifest.json +255 -90
  23. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  24. package/dist/client/_assets/chunks/create-editor-B-m7X7S5.js +261 -0
  25. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  26. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  27. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  28. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  29. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  30. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  31. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  32. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  33. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  34. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  35. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  36. package/dist/client/_assets/chunks/sortable-list-BJyd-LXE.js +2 -0
  37. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  38. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  39. package/dist/client/_assets/chunks/unsafe-svg-BkscJx69.js +2 -0
  40. package/dist/client/_assets/client-DAhoqPdr.css +2 -0
  41. package/dist/client/_assets/client-auth-CAMfTrKW.js +828 -0
  42. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  43. package/dist/client/_assets/client-compose-D6rExKVK.js +2004 -0
  44. package/dist/client/_assets/client-knSEyUJO.js +295 -0
  45. package/dist/client/_assets/client-manage-sVOkqVbH.js +2170 -0
  46. package/dist/client/_assets/client-settings-CGR_nZ78.js +1112 -0
  47. package/dist/github-sync-Gw4orAHk.js +8398 -0
  48. package/dist/index.js +2 -5
  49. package/dist/node.js +11 -9
  50. package/package.json +15 -5
  51. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  52. package/src/__tests__/basecoat-variants.test.ts +121 -0
  53. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  54. package/src/__tests__/dev-scripts.test.ts +203 -0
  55. package/src/__tests__/export-collection-order.test.ts +276 -0
  56. package/src/__tests__/export-feed-ids.test.ts +184 -0
  57. package/src/__tests__/export-feed-order.test.ts +294 -0
  58. package/src/__tests__/export-hugo-build.test.ts +390 -0
  59. package/src/__tests__/export-import-roundtrip.test.ts +94 -0
  60. package/src/__tests__/export-service.test.ts +842 -28
  61. package/src/__tests__/export-smart-collection.test.ts +329 -0
  62. package/src/__tests__/helpers/component-css.ts +91 -0
  63. package/src/__tests__/helpers/db.ts +33 -0
  64. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  65. package/src/__tests__/helpers/hugo-site.ts +146 -0
  66. package/src/__tests__/import-site-command.test.ts +487 -1
  67. package/src/__tests__/mise-config.test.ts +75 -4
  68. package/src/__tests__/node-dev-tasks.test.ts +264 -0
  69. package/src/__tests__/site-export-canonical-import.test.ts +149 -0
  70. package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
  71. package/src/__tests__/snapshot-settings.test.ts +97 -0
  72. package/src/__tests__/snapshot-tables.test.ts +219 -0
  73. package/src/__tests__/sql-export.test.ts +173 -0
  74. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  75. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  76. package/src/__tests__/zip-archive.test.ts +106 -0
  77. package/src/app.tsx +86 -22
  78. package/src/auth.ts +18 -1
  79. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  80. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  81. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  82. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  83. package/src/client/__tests__/compose-launch.test.ts +62 -1
  84. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  85. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  86. package/src/client/__tests__/copy-field.test.ts +162 -0
  87. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  88. package/src/client/__tests__/image-processor.test.ts +324 -2
  89. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  90. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  91. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  92. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  93. package/src/client/__tests__/toast.test.ts +23 -0
  94. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  95. package/src/client/__tests__/video-processor.test.ts +181 -0
  96. package/src/client/audio-processor.ts +24 -15
  97. package/src/client/audio-waveform.ts +49 -0
  98. package/src/client/collection-dialog-host.ts +72 -0
  99. package/src/client/collection-navigation.ts +51 -16
  100. package/src/client/collection-page-actions.ts +26 -0
  101. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  102. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  103. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  104. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  105. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  106. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  107. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  108. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  109. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  110. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  111. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  112. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  113. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  114. package/src/client/components/__tests__/jant-settings-general.test.ts +561 -172
  115. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  116. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  117. package/src/client/components/collection-dialog-types.ts +49 -0
  118. package/src/client/components/collection-manager-types.ts +32 -1
  119. package/src/client/components/collection-types.ts +1 -15
  120. package/src/client/components/compose-types.ts +75 -1
  121. package/src/client/components/jant-collection-dialog.ts +722 -0
  122. package/src/client/components/jant-collection-directory.ts +567 -182
  123. package/src/client/components/jant-collection-form.ts +53 -232
  124. package/src/client/components/jant-command-palette.ts +100 -49
  125. package/src/client/components/jant-compose-dialog.ts +1486 -707
  126. package/src/client/components/jant-compose-editor.ts +673 -336
  127. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  128. package/src/client/components/jant-config-editor.ts +65 -53
  129. package/src/client/components/jant-confirm-dialog.ts +13 -11
  130. package/src/client/components/jant-locale-picker.ts +418 -0
  131. package/src/client/components/jant-media-lightbox.ts +146 -136
  132. package/src/client/components/jant-nav-manager.ts +690 -366
  133. package/src/client/components/jant-post-menu.ts +1086 -282
  134. package/src/client/components/jant-post-picker.ts +324 -0
  135. package/src/client/components/jant-repo-picker-types.ts +6 -1
  136. package/src/client/components/jant-repo-picker.ts +137 -108
  137. package/src/client/components/jant-settings-avatar.ts +31 -27
  138. package/src/client/components/jant-settings-general.ts +622 -543
  139. package/src/client/components/jant-settings-language.ts +1087 -0
  140. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  141. package/src/client/components/jant-text-preview.ts +94 -86
  142. package/src/client/components/nav-manager-types.ts +62 -1
  143. package/src/client/components/settings-types.ts +14 -29
  144. package/src/client/components/smart-collection-conditions.ts +285 -0
  145. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  146. package/src/client/compose-bridge.ts +214 -141
  147. package/src/client/compose-launch.ts +76 -2
  148. package/src/client/compose-shortcuts.ts +6 -8
  149. package/src/client/compose-triggers.ts +50 -0
  150. package/src/client/copy-field.ts +108 -0
  151. package/src/client/custom-url-menu.ts +60 -4
  152. package/src/client/icons.ts +14 -0
  153. package/src/client/image-plan.ts +165 -0
  154. package/src/client/image-processor.ts +195 -117
  155. package/src/client/image-worker-client.ts +45 -0
  156. package/src/client/image-worker.ts +135 -0
  157. package/src/client/lazy-entries.ts +95 -0
  158. package/src/client/lazy-slugify.ts +2 -2
  159. package/src/client/media-scroll-hint.ts +43 -2
  160. package/src/client/mediabunny.ts +29 -0
  161. package/src/client/post-picker.ts +41 -0
  162. package/src/client/runtime-paths.ts +74 -1
  163. package/src/client/settings-bridge.ts +4 -10
  164. package/src/client/site-header-nav.js +18 -6
  165. package/src/client/smart-collection-dialog-host.ts +116 -0
  166. package/src/client/smart-collection-page-actions.ts +208 -0
  167. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  168. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  169. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  170. package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
  171. package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
  172. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
  173. package/src/client/tiptap/bubble-menu.ts +47 -16
  174. package/src/client/tiptap/extensions.ts +0 -3
  175. package/src/client/tiptap/floating-position.ts +216 -2
  176. package/src/client/tiptap/inline-image-upload.ts +7 -3
  177. package/src/client/tiptap/link-toolbar.ts +22 -9
  178. package/src/client/tiptap/structural-keymap.ts +158 -47
  179. package/src/client/toast.ts +13 -0
  180. package/src/client/types/vite-worker.d.ts +12 -0
  181. package/src/client/upload-with-metadata.ts +3 -13
  182. package/src/client/video-processor.ts +351 -107
  183. package/src/client-auth.ts +14 -31
  184. package/src/client-compose.ts +19 -0
  185. package/src/client-manage.ts +13 -0
  186. package/src/client-settings.ts +14 -0
  187. package/src/client-site.ts +10 -3
  188. package/src/client.ts +4 -0
  189. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  190. package/src/db/__tests__/d1-query.test.ts +56 -1
  191. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  192. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  193. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  194. package/src/db/migrations/0032_married_tomas.sql +3 -0
  195. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  196. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  197. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  198. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  199. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  200. package/src/db/migrations/meta/_journal.json +21 -0
  201. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  202. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  203. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  204. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  205. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  206. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  207. package/src/db/migrations/pg/meta/_journal.json +21 -0
  208. package/src/db/pg/schema.ts +195 -27
  209. package/src/db/post-visibility.ts +117 -0
  210. package/src/db/schema.ts +222 -27
  211. package/src/i18n/__tests__/detect.test.ts +26 -81
  212. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  213. package/src/i18n/context.tsx +12 -2
  214. package/src/i18n/detect.ts +7 -66
  215. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  216. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  217. package/src/i18n/locales/public/en.po +490 -158
  218. package/src/i18n/locales/public/en.ts +1 -1
  219. package/src/i18n/locales/public/zh-Hans.po +487 -155
  220. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  221. package/src/i18n/locales/public/zh-Hant.po +487 -155
  222. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  223. package/src/i18n/locales/settings/en.po +555 -73
  224. package/src/i18n/locales/settings/en.ts +1 -1
  225. package/src/i18n/locales/settings/zh-Hans.po +601 -119
  226. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  227. package/src/i18n/locales/settings/zh-Hant.po +614 -132
  228. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  229. package/src/i18n/locales.ts +94 -0
  230. package/src/i18n/middleware.ts +12 -2
  231. package/src/i18n/supported-locales.ts +80 -0
  232. package/src/lib/__tests__/api-settings.test.ts +11 -2
  233. package/src/lib/__tests__/asset-path.test.ts +92 -0
  234. package/src/lib/__tests__/deferred.test.ts +90 -0
  235. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  236. package/src/lib/__tests__/discover.test.ts +410 -0
  237. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  238. package/src/lib/__tests__/feed.test.ts +1526 -15
  239. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  240. package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
  241. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  242. package/src/lib/__tests__/http-cache.test.ts +162 -0
  243. package/src/lib/__tests__/image.test.ts +27 -1
  244. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  245. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  246. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  247. package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
  248. package/src/lib/__tests__/markdown.test.ts +10 -0
  249. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  250. package/src/lib/__tests__/navigation.test.ts +135 -1
  251. package/src/lib/__tests__/resolve-config.test.ts +131 -3
  252. package/src/lib/__tests__/schemas.test.ts +69 -5
  253. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  254. package/src/lib/__tests__/slug.test.ts +70 -0
  255. package/src/lib/__tests__/slugify.test.ts +85 -0
  256. package/src/lib/__tests__/storage.test.ts +32 -0
  257. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  258. package/src/lib/__tests__/timeline.test.ts +87 -0
  259. package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
  260. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  261. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  262. package/src/lib/__tests__/url.test.ts +78 -72
  263. package/src/lib/__tests__/view-language.test.ts +413 -0
  264. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  265. package/src/lib/address.ts +57 -0
  266. package/src/lib/api-posts.ts +12 -0
  267. package/src/lib/asset-path.ts +98 -1
  268. package/src/lib/build-env.ts +16 -0
  269. package/src/lib/collection-groups.ts +5 -1
  270. package/src/lib/collection-paths.ts +3 -10
  271. package/src/lib/constants.ts +28 -3
  272. package/src/lib/copy-field.ts +36 -0
  273. package/src/lib/crypto.ts +40 -0
  274. package/src/lib/csp-builder.ts +2 -1
  275. package/src/lib/deferred.ts +79 -0
  276. package/src/lib/discover-ping.ts +141 -0
  277. package/src/lib/discover.ts +439 -0
  278. package/src/lib/env.ts +134 -0
  279. package/src/lib/errors.ts +33 -0
  280. package/src/lib/feed-policy.ts +236 -2
  281. package/src/lib/feed.ts +926 -73
  282. package/src/lib/filter-dimensions.ts +1349 -0
  283. package/src/lib/github-sync-repo-name.ts +45 -0
  284. package/src/lib/github-sync-site-config.ts +3 -0
  285. package/src/lib/hosted-signin.ts +1 -1
  286. package/src/lib/http-cache.ts +148 -0
  287. package/src/lib/hugo-markdown.ts +62 -0
  288. package/src/lib/icons.ts +9 -2
  289. package/src/lib/ids.ts +3 -0
  290. package/src/lib/image.ts +17 -4
  291. package/src/lib/jant-docs.ts +42 -0
  292. package/src/lib/lang-detect.ts +308 -0
  293. package/src/lib/markdown-manager.ts +392 -2
  294. package/src/lib/media-helpers.ts +13 -1
  295. package/src/lib/navigation.ts +78 -11
  296. package/src/lib/pagination.ts +28 -24
  297. package/src/lib/per-language-surfaces.ts +67 -0
  298. package/src/lib/post-body-html.ts +11 -4
  299. package/src/lib/post-meta.ts +19 -0
  300. package/src/lib/render.tsx +55 -7
  301. package/src/lib/resolve-config.ts +111 -6
  302. package/src/lib/schemas.ts +251 -15
  303. package/src/lib/site-header-fragment.tsx +2 -1
  304. package/src/lib/site-unavailable-page.ts +47 -0
  305. package/src/lib/slug.ts +51 -7
  306. package/src/lib/slugify.ts +113 -0
  307. package/src/lib/startup-config.ts +4 -6
  308. package/src/lib/storage.ts +7 -0
  309. package/src/lib/summary.ts +50 -0
  310. package/src/lib/thread-fold.ts +161 -0
  311. package/src/lib/time.ts +32 -0
  312. package/src/lib/timeline.ts +43 -2
  313. package/src/lib/tiptap-render.ts +1 -2
  314. package/src/lib/tiptap-to-markdown.ts +13 -7
  315. package/src/lib/upload.ts +16 -13
  316. package/src/lib/url.ts +159 -86
  317. package/src/lib/version.ts +10 -8
  318. package/src/lib/view-language.ts +436 -0
  319. package/src/lib/view.ts +53 -25
  320. package/src/lib/viewer-context.ts +62 -0
  321. package/src/middleware/__tests__/auth.test.ts +70 -2
  322. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  323. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  324. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  325. package/src/middleware/__tests__/session.test.ts +181 -33
  326. package/src/middleware/auth.ts +50 -31
  327. package/src/middleware/cache-control.ts +75 -10
  328. package/src/middleware/config.ts +6 -6
  329. package/src/middleware/error-handler.ts +12 -0
  330. package/src/middleware/onboarding.ts +32 -10
  331. package/src/middleware/secure-headers.ts +1 -1
  332. package/src/middleware/session.ts +96 -8
  333. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  334. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  335. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  336. package/src/node/__tests__/cli-setup.test.ts +163 -0
  337. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  338. package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
  339. package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
  340. package/src/node/__tests__/runtime.test.ts +75 -0
  341. package/src/node/index.ts +3 -0
  342. package/src/node/request-handler.ts +3 -1
  343. package/src/preset.css +76 -22
  344. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  345. package/src/routes/api/__tests__/collections.test.ts +114 -0
  346. package/src/routes/api/__tests__/discover.test.ts +222 -0
  347. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  348. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  349. package/src/routes/api/__tests__/posts.test.ts +24 -0
  350. package/src/routes/api/__tests__/settings.test.ts +24 -3
  351. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  352. package/src/routes/api/__tests__/upload.test.ts +34 -0
  353. package/src/routes/api/collections.ts +49 -6
  354. package/src/routes/api/custom-urls.ts +0 -1
  355. package/src/routes/api/discover.ts +95 -0
  356. package/src/routes/api/export.ts +6 -3
  357. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  358. package/src/routes/api/internal/sites.ts +0 -1
  359. package/src/routes/api/nav-items.ts +55 -8
  360. package/src/routes/api/posts.ts +161 -0
  361. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  362. package/src/routes/api/public/archive.ts +77 -81
  363. package/src/routes/api/public/posts.ts +44 -7
  364. package/src/routes/api/smart-collections.ts +132 -0
  365. package/src/routes/api/telegram.ts +2 -40
  366. package/src/routes/api/upload.ts +10 -3
  367. package/src/routes/api/uploads.ts +1 -2
  368. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  369. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  370. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  371. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  372. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  373. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  374. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  375. package/src/routes/auth/setup.tsx +934 -186
  376. package/src/routes/auth/signin.tsx +10 -8
  377. package/src/routes/compose.tsx +9 -0
  378. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  379. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  380. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  381. package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
  382. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  383. package/src/routes/dash/custom-urls.tsx +1 -37
  384. package/src/routes/dash/settings.tsx +714 -111
  385. package/src/routes/discover-announce.ts +96 -0
  386. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  387. package/src/routes/feed/feed.ts +69 -40
  388. package/src/routes/feed/sitemap.ts +95 -11
  389. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  390. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  391. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  392. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  393. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  394. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  395. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  396. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  397. package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
  398. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  399. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  400. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  401. package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
  402. package/src/routes/pages/archive.tsx +487 -379
  403. package/src/routes/pages/collection.tsx +94 -96
  404. package/src/routes/pages/collections.tsx +38 -50
  405. package/src/routes/pages/featured.tsx +43 -18
  406. package/src/routes/pages/home.tsx +25 -6
  407. package/src/routes/pages/language.tsx +113 -0
  408. package/src/routes/pages/latest.tsx +26 -16
  409. package/src/routes/pages/new.tsx +3 -0
  410. package/src/routes/pages/page.tsx +228 -45
  411. package/src/routes/pages/partials.tsx +41 -0
  412. package/src/routes/pages/search.tsx +22 -5
  413. package/src/routes/pages/smart-collection.tsx +340 -0
  414. package/src/routes/pages/subscribe.tsx +182 -0
  415. package/src/runtime/__tests__/index.test.ts +19 -0
  416. package/src/runtime/__tests__/node.test.ts +129 -4
  417. package/src/runtime/__tests__/readiness.test.ts +23 -0
  418. package/src/runtime/cloudflare.ts +21 -2
  419. package/src/runtime/index.ts +27 -1
  420. package/src/runtime/node.ts +67 -1
  421. package/src/runtime/readiness.ts +15 -0
  422. package/src/runtime/site.ts +95 -24
  423. package/src/services/__tests__/auth.test.ts +116 -7
  424. package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
  425. package/src/services/__tests__/collection.test.ts +108 -4
  426. package/src/services/__tests__/custom-url.test.ts +34 -0
  427. package/src/services/__tests__/github-app-installations.test.ts +153 -0
  428. package/src/services/__tests__/github-sync-push.test.ts +46 -0
  429. package/src/services/__tests__/language.test.ts +495 -0
  430. package/src/services/__tests__/media.test.ts +31 -0
  431. package/src/services/__tests__/navigation.test.ts +213 -10
  432. package/src/services/__tests__/path.test.ts +236 -0
  433. package/src/services/__tests__/post-language.test.ts +830 -0
  434. package/src/services/__tests__/post-timeline.test.ts +127 -0
  435. package/src/services/__tests__/post.test.ts +120 -9
  436. package/src/services/__tests__/settings.test.ts +228 -17
  437. package/src/services/__tests__/site-admin.test.ts +5 -5
  438. package/src/services/__tests__/smart-collection.test.ts +651 -0
  439. package/src/services/about-page.ts +22 -1
  440. package/src/services/auth.ts +51 -49
  441. package/src/services/bootstrap.ts +408 -39
  442. package/src/services/collection-directory-position.ts +76 -0
  443. package/src/services/collection.ts +331 -46
  444. package/src/services/custom-url.ts +29 -24
  445. package/src/services/export-theme/assets/client-site.css +1 -1
  446. package/src/services/export-theme/assets/client-site.js +123 -123
  447. package/src/services/export-theme/layouts/_default/alias.html +27 -1
  448. package/src/services/export-theme/layouts/_default/list.html +2 -63
  449. package/src/services/export-theme/layouts/_default/rss.xml +103 -38
  450. package/src/services/export-theme/layouts/collections/list.html +3 -3
  451. package/src/services/export-theme/layouts/featured/list.html +1 -4
  452. package/src/services/export-theme/layouts/index.html +40 -26
  453. package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
  454. package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
  455. package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
  456. package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
  457. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  458. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  459. package/src/services/export-theme/layouts/partials/head.html +1 -1
  460. package/src/services/export-theme/layouts/partials/header.html +5 -3
  461. package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
  462. package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
  463. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  464. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  465. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  466. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  467. package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
  468. package/src/services/export-theme/layouts/partials/thread-preview.html +41 -31
  469. package/src/services/export-theme/layouts/post/list.html +1 -1
  470. package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
  471. package/src/services/export-theme/styles/main.css +13 -2
  472. package/src/services/export-theme/theme.toml +1 -1
  473. package/src/services/export.ts +880 -72
  474. package/src/services/github-app-installations.ts +171 -4
  475. package/src/services/github-sync.ts +69 -15
  476. package/src/services/index.ts +34 -3
  477. package/src/services/language.ts +412 -0
  478. package/src/services/mcp.ts +22 -2
  479. package/src/services/media.ts +22 -7
  480. package/src/services/navigation.ts +429 -67
  481. package/src/services/path.ts +254 -6
  482. package/src/services/post.ts +1234 -190
  483. package/src/services/search.ts +39 -2
  484. package/src/services/settings.ts +238 -43
  485. package/src/services/site-admin.ts +27 -14
  486. package/src/services/site.ts +160 -32
  487. package/src/services/smart-collection.ts +636 -0
  488. package/src/services/telegram.ts +20 -22
  489. package/src/services/upload-session.ts +1 -3
  490. package/src/style-author.css +27 -0
  491. package/src/styles/components-author.css +1622 -0
  492. package/src/styles/components.css +24 -1571
  493. package/src/styles/site-media.css +16 -2
  494. package/src/styles/tokens.css +82 -12
  495. package/src/styles/ui-author.css +7602 -0
  496. package/src/styles/ui.css +1090 -7216
  497. package/src/types/app-context.ts +23 -0
  498. package/src/types/bindings.ts +8 -0
  499. package/src/types/config.ts +94 -15
  500. package/src/types/constants.ts +188 -16
  501. package/src/types/entities.ts +87 -1
  502. package/src/types/operations.ts +74 -2
  503. package/src/types/props.ts +157 -31
  504. package/src/types/views.ts +55 -1
  505. package/src/ui/__tests__/font-themes.test.ts +52 -12
  506. package/src/ui/compose/ComposeDialog.tsx +201 -7
  507. package/src/ui/compose/ComposePrompt.tsx +1 -5
  508. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  509. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  510. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  511. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  512. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  513. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  514. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  515. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  516. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  517. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  518. package/src/ui/dash/settings/GeneralContent.tsx +277 -113
  519. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  520. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  521. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  522. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  523. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  524. package/src/ui/dash/settings/discover-copy.ts +116 -0
  525. package/src/ui/feed/LinkCard.tsx +55 -45
  526. package/src/ui/feed/NoteCard.tsx +9 -4
  527. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  528. package/src/ui/feed/QuoteCard.tsx +55 -30
  529. package/src/ui/feed/ThreadPreview.tsx +6 -2
  530. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  531. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  532. package/src/ui/feed/thread-preview-state.ts +20 -6
  533. package/src/ui/font-themes.ts +39 -29
  534. package/src/ui/layouts/BaseLayout.tsx +155 -10
  535. package/src/ui/layouts/SiteLayout.tsx +192 -34
  536. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  537. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  538. package/src/ui/pages/ArchivePage.tsx +282 -289
  539. package/src/ui/pages/CollectionPage.tsx +86 -76
  540. package/src/ui/pages/CollectionsPage.tsx +5 -0
  541. package/src/ui/pages/ComposePage.tsx +8 -0
  542. package/src/ui/pages/PostPage.tsx +47 -0
  543. package/src/ui/pages/SearchPage.tsx +3 -3
  544. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  545. package/src/ui/pages/SubscribePage.tsx +236 -0
  546. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  547. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  548. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  549. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  550. package/src/ui/shared/CollectionsManager.tsx +70 -17
  551. package/src/ui/shared/CopyField.tsx +127 -0
  552. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  553. package/src/ui/shared/MediaGallery.tsx +125 -46
  554. package/src/ui/shared/Pagination.tsx +20 -5
  555. package/src/ui/shared/PostFooter.tsx +20 -11
  556. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  557. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  558. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  559. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  560. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  561. package/src/ui/shared/archive-labels.ts +98 -0
  562. package/src/ui/shared/collection-management-labels.ts +94 -16
  563. package/src/ui/shared/navigation-labels.ts +45 -71
  564. package/src/ui/shared/post-article-attributes.ts +2 -0
  565. package/src/ui/shared/smart-collection-labels.ts +343 -0
  566. package/dist/app-C75Zc8dD.js +0 -6
  567. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  568. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  569. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  570. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  571. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  572. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  573. package/dist/env-BPYViDJJ.js +0 -281
  574. package/dist/export-Dl5KCiEs.js +0 -5562
  575. package/dist/github-api-BNF35Lkx.js +0 -176
  576. package/dist/github-app-DdiD-bC4.js +0 -275
  577. package/dist/github-sync-Bqg62IvV.js +0 -4
  578. package/dist/github-sync-HdK2EgvJ.js +0 -431
  579. package/dist/url-CbLAtlZe.js +0 -900
  580. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  581. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  582. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  583. package/src/client/collection-created-notice.ts +0 -66
  584. package/src/client/collection-form-bridge.ts +0 -191
  585. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  586. package/src/client/components/jant-post-form.ts +0 -422
  587. package/src/client/components/post-form-template.ts +0 -352
  588. package/src/client/components/post-form-types.ts +0 -96
  589. package/src/client/media-metadata.ts +0 -247
  590. package/src/client/post-form-bridge.ts +0 -193
  591. package/src/client/tiptap/exitable-marks.ts +0 -73
  592. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -14,6 +14,7 @@ import { SigninSchema } from "../../lib/schemas.js";
14
14
  import { buildPageTitle } from "../../lib/page-title.js";
15
15
  import { getI18n } from "../../i18n/index.js";
16
16
  import { getHostedControlPlaneSigninUrl } from "../../lib/hosted-signin.js";
17
+ import { isCurrentSiteMember } from "../../middleware/auth.js";
17
18
  import { isSafeInternalRedirect, toPublicPath } from "../../lib/url.js";
18
19
 
19
20
  type Env = { Bindings: Bindings; Variables: AppVariables };
@@ -128,7 +129,10 @@ signinRoutes.get("/signin", async (c) => {
128
129
  ? rawRedirect
129
130
  : undefined;
130
131
 
131
- if (c.var.isAuthenticated) {
132
+ // Membership, not merely a session: `requireAuth` sends non-members here, so
133
+ // reading `isAuthenticated` instead would send them straight back and the two
134
+ // would volley until the browser refused to follow another hop.
135
+ if (await isCurrentSiteMember(c)) {
132
136
  return c.redirect(
133
137
  toPublicPath(redirect ?? "/", c.var.appConfig.sitePathPrefix),
134
138
  );
@@ -144,14 +148,12 @@ signinRoutes.get("/signin", async (c) => {
144
148
  }
145
149
 
146
150
  const i18n = getI18n(c);
147
- const isSetup = c.req.query("setup") !== undefined;
151
+ // No `?setup` counterpart: setup signs its new owner in on the spot and
152
+ // hands them straight to the next screen, so nobody arrives here from it.
148
153
  const isReset = c.req.query("reset") !== undefined;
149
- let toast: { message: string } | undefined;
150
- if (isSetup) {
151
- toast = { message: "Account created. Sign in to get started." };
152
- } else if (isReset) {
153
- toast = { message: "Password reset. Sign in with your new password." };
154
- }
154
+ const toast = isReset
155
+ ? { message: "Password reset. Sign in with your new password." }
156
+ : undefined;
155
157
 
156
158
  return c.html(
157
159
  <BaseLayout
@@ -89,7 +89,11 @@ composeRoutes.post("/", async (c) => {
89
89
  collectionIds: data.collectionIds,
90
90
  replyToId: data.replyToId,
91
91
  quietReply: data.quietReply,
92
+ language: data.language,
93
+ translationOfId: data.translationOfId,
92
94
  publishedAt: data.publishedAt,
95
+ createdAt: data.createdAt,
96
+ updatedAt: data.updatedAt,
93
97
  },
94
98
  data.attachments,
95
99
  {
@@ -231,9 +235,14 @@ composeRoutes.post("/thread", async (c) => {
231
235
  collectionIds: index === 0 ? data.collectionIds : undefined,
232
236
  replyToId: index === 0 ? data.replyToId : undefined,
233
237
  quietReply: data.quietReply,
238
+ // Thread-level, so only the root states them; replies inherit.
239
+ language: index === 0 ? data.language : undefined,
240
+ translationOfId: index === 0 ? data.translationOfId : undefined,
234
241
  // Replies may carry their own date; when they don't, the service
235
242
  // inherits the root's rather than stamping "now".
236
243
  publishedAt: data.publishedAt,
244
+ createdAt: data.createdAt,
245
+ updatedAt: data.updatedAt,
237
246
  },
238
247
  attachments: data.attachments,
239
248
  })),
@@ -0,0 +1,44 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createTestApp } from "../../../__tests__/helpers/app.js";
3
+
4
+ /**
5
+ * The archive custom URL is readable forever and creatable never.
6
+ *
7
+ * Hand-typing `format=note&title=none` into a settings text field is the
8
+ * problem smart collections replace. Removing it from the form is not the same
9
+ * as removing it — an endpoint that still accepts it has not stopped offering
10
+ * it — so the refusal lives in the schema and again in the service.
11
+ */
12
+ describe("archive custom URLs", () => {
13
+ it("refuses to create one", async () => {
14
+ const { services } = createTestApp({ authenticated: true });
15
+
16
+ await expect(
17
+ services.customUrls.create({
18
+ path: "/notes-only",
19
+ targetType: "archive",
20
+ archiveQuery: "format=note",
21
+ }),
22
+ ).rejects.toThrow(/smart collection/i);
23
+ });
24
+
25
+ it("still reads, lists, and deletes one that already exists", async () => {
26
+ const { services } = createTestApp({ authenticated: true });
27
+
28
+ // How a real legacy row exists: written before the create path was closed.
29
+ const record = await services.paths.create({
30
+ path: "/notes-only",
31
+ kind: "archive",
32
+ archiveQuery: "format=note&title=none",
33
+ });
34
+
35
+ const listed = await services.customUrls.list();
36
+ expect(listed.map((entry) => entry.path)).toContain("notes-only");
37
+ expect(
38
+ listed.find((entry) => entry.path === "notes-only")?.targetType,
39
+ ).toBe("archive");
40
+
41
+ expect(await services.customUrls.delete(record.id)).toBe(true);
42
+ expect(await services.paths.getByPath("/notes-only")).toBeNull();
43
+ });
44
+ });
@@ -0,0 +1,49 @@
1
+ /**
2
+ * What the Custom URLs settings page shows for a legacy archive path.
3
+ *
4
+ * The row is the whole affordance: it prints the query the path holds, and its
5
+ * menu deletes. It deliberately offers no "turn into a smart collection" —
6
+ * that row *is* the registration of its own address, so the collection it would
7
+ * become could never be saved at the URL the offer was about.
8
+ */
9
+
10
+ import { describe, expect, it } from "vitest";
11
+ import { createTestApp } from "../../../__tests__/helpers/app.js";
12
+ import { customUrlsRoutes } from "../custom-urls.js";
13
+
14
+ function createCustomUrlsTestApp() {
15
+ const testApp = createTestApp({ authenticated: true });
16
+ const { app } = testApp;
17
+
18
+ app.use("*", async (c, next) => {
19
+ c.set("publicPath", c.req.path);
20
+ c.set("publicRequestUrl", c.req.url);
21
+ await next();
22
+ });
23
+
24
+ app.route("/settings/custom-urls", customUrlsRoutes);
25
+
26
+ return testApp;
27
+ }
28
+
29
+ describe("custom URLs settings page", () => {
30
+ it("shows a legacy archive path and offers only deletion", async () => {
31
+ const { app, services } = createCustomUrlsTestApp();
32
+
33
+ // How a real legacy row exists: written before the create path was closed.
34
+ await services.paths.create({
35
+ path: "/notes-only",
36
+ kind: "archive",
37
+ archiveQuery: "format=note&title=none",
38
+ });
39
+
40
+ const res = await app.request("/settings/custom-urls");
41
+ expect(res.status).toBe(200);
42
+
43
+ const html = await res.text();
44
+ expect(html).toContain("/notes-only");
45
+ expect(html).toContain("/archive?format=note&amp;title=none");
46
+ expect(html).toContain('data-custom-url-action="delete"');
47
+ expect(html).not.toContain('data-custom-url-action="upgrade"');
48
+ });
49
+ });
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Tests for POST /settings/account/delete-account.
3
+ *
4
+ * The route is a POST returning a Datastar redirect, so it can be exercised
5
+ * end to end without the SSR setup the settings pages need.
6
+ */
7
+
8
+ import { describe, expect, it, vi } from "vitest";
9
+ import { Hono } from "hono";
10
+ import { createI18n } from "../../../i18n/index.js";
11
+ import { settingsRoutes } from "../settings.js";
12
+ import type { Bindings } from "../../../types.js";
13
+ import type { AppVariables } from "../../../types/app-context.js";
14
+
15
+ type Env = { Bindings: Bindings; Variables: AppVariables };
16
+
17
+ const CLEARED_SESSION_COOKIE =
18
+ "better-auth.session_token=; Path=/; Max-Age=0; HttpOnly";
19
+
20
+ function createDeleteAccountTestApp() {
21
+ const calls: string[] = [];
22
+
23
+ const signOut = vi.fn(async () => {
24
+ calls.push("signOut");
25
+ return new Response(null, {
26
+ headers: { "Set-Cookie": CLEARED_SESSION_COOKIE },
27
+ });
28
+ });
29
+ const deleteAllData = vi.fn(async () => {
30
+ calls.push("deleteAllData");
31
+ });
32
+
33
+ const app = new Hono<Env>();
34
+ app.use("*", async (c, next) => {
35
+ c.env = {} as Bindings;
36
+ c.set("auth", { api: { signOut } } as unknown as AppVariables["auth"]);
37
+ c.set("services", {
38
+ auth: {
39
+ validateDeleteCsrfToken: async (_token: string, sessionId: string) =>
40
+ sessionId === "ses_test",
41
+ deleteAllData,
42
+ },
43
+ } as unknown as AppVariables["services"]);
44
+ c.set("storage", null);
45
+ c.set("session", {
46
+ session: { id: "ses_test" },
47
+ user: { id: "usr_test" },
48
+ } as AppVariables["session"]);
49
+ c.set("isAuthenticated", true);
50
+ c.set("currentSite", {
51
+ createdAt: 0,
52
+ id: "sit_test",
53
+ key: "default",
54
+ status: "active",
55
+ updatedAt: 0,
56
+ });
57
+ c.set("appConfig", {
58
+ siteName: "Jant",
59
+ sitePathPrefix: "",
60
+ } as AppVariables["appConfig"]);
61
+ c.set("lang", "en");
62
+ c.set("i18n", createI18n("en"));
63
+ await next();
64
+ });
65
+ app.route("/settings", settingsRoutes);
66
+
67
+ return { app, calls, signOut, deleteAllData };
68
+ }
69
+
70
+ function requestDelete(app: Hono<Env>) {
71
+ return app.request("/settings/account/delete-account", {
72
+ method: "POST",
73
+ headers: { "x-csrf-token": "valid-token" },
74
+ });
75
+ }
76
+
77
+ describe("POST /settings/account/delete-account", () => {
78
+ it("clears the session cookie so the browser keeps no ticket to the deleted site", async () => {
79
+ const { app } = createDeleteAccountTestApp();
80
+
81
+ const res = await requestDelete(app);
82
+
83
+ expect(res.status).toBe(200);
84
+ expect(res.headers.getSetCookie()).toContain(CLEARED_SESSION_COOKIE);
85
+ await expect(res.text()).resolves.toContain("/setup");
86
+ });
87
+
88
+ it("signs out before the wipe, while the session row still exists", async () => {
89
+ const { app, calls } = createDeleteAccountTestApp();
90
+
91
+ await requestDelete(app);
92
+
93
+ expect(calls).toEqual(["signOut", "deleteAllData"]);
94
+ });
95
+
96
+ it("still wipes the site when signing out fails", async () => {
97
+ const { app, calls, signOut, deleteAllData } = createDeleteAccountTestApp();
98
+ signOut.mockRejectedValueOnce(new Error("no session"));
99
+
100
+ const res = await requestDelete(app);
101
+
102
+ expect(res.status).toBe(200);
103
+ expect(calls).toEqual(["deleteAllData"]);
104
+ expect(deleteAllData).toHaveBeenCalledOnce();
105
+ });
106
+ });
@@ -0,0 +1,365 @@
1
+ /**
2
+ * GitHub App connect flow: who may see and use an installation.
3
+ *
4
+ * A GitHub App installs once per GitHub account, so the author of a
5
+ * second site cannot install it again — GitHub sends them to the
6
+ * existing installation's Configure page and never calls back. These
7
+ * tests pin the way out of that: installations follow the author across
8
+ * the sites they belong to, and stop at every other tenant.
9
+ */
10
+
11
+ import { beforeEach, describe, expect, it, vi } from "vitest";
12
+ import { createTestApp } from "../../../__tests__/helpers/app.js";
13
+ import { DEFAULT_TEST_SITE_ID } from "../../../__tests__/helpers/db.js";
14
+ import { settingsRoutes } from "../settings.js";
15
+
16
+ const SECOND_SITE_ID = "sit_second00000000000000000000000";
17
+ const OTHER_TENANT_SITE_ID = "sit_other000000000000000000000000";
18
+ /** The user id `createTestApp({ authenticated: true })` signs in as. */
19
+ const AUTHOR_ID = "test-user";
20
+
21
+ const setupWebhook = vi.fn();
22
+ const pushFullSync = vi.fn();
23
+ const classifyRepoForSync = vi.fn();
24
+ const getInstallation = vi.fn();
25
+
26
+ vi.mock("../../../services/github-sync.js", () => ({
27
+ classifyRepoForSync: (...args: unknown[]) => classifyRepoForSync(...args),
28
+ createGitHubSyncService: () => ({
29
+ setupWebhook,
30
+ pushFullSync,
31
+ }),
32
+ }));
33
+
34
+ vi.mock("../../../lib/github-api.js", async (importOriginal) => ({
35
+ ...(await importOriginal<typeof import("../../../lib/github-api.js")>()),
36
+ createGitHubClient: () => ({}),
37
+ }));
38
+
39
+ vi.mock("../../../lib/github-app.js", async (importOriginal) => ({
40
+ ...(await importOriginal<typeof import("../../../lib/github-app.js")>()),
41
+ getInstallation: (...args: unknown[]) => getInstallation(...args),
42
+ }));
43
+
44
+ function createGitHubApp(options: { authenticated?: boolean } = {}) {
45
+ const harness = createTestApp({
46
+ authenticated: options.authenticated ?? true,
47
+ });
48
+ harness.app.use("*", async (c, next) => {
49
+ c.env.GITHUB_APP_ID = "12345";
50
+ c.env.GITHUB_APP_PRIVATE_KEY =
51
+ "-----BEGIN PRIVATE KEY-----\nx\n-----END PRIVATE KEY-----";
52
+ c.env.GITHUB_APP_SLUG = "jant-sync";
53
+ await next();
54
+ });
55
+ harness.app.route("/settings", settingsRoutes);
56
+ return harness;
57
+ }
58
+
59
+ function insertSite(
60
+ sqlite: ReturnType<typeof createTestApp>["sqlite"],
61
+ siteId: string,
62
+ key: string,
63
+ ) {
64
+ const timestamp = Math.floor(Date.now() / 1000);
65
+ sqlite
66
+ .prepare(
67
+ `INSERT INTO site (id, key, status, created_at, updated_at)
68
+ VALUES (?, ?, 'active', ?, ?)`,
69
+ )
70
+ .run(siteId, key, timestamp, timestamp);
71
+ }
72
+
73
+ function insertMember(
74
+ sqlite: ReturnType<typeof createTestApp>["sqlite"],
75
+ siteId: string,
76
+ userId: string,
77
+ ) {
78
+ const timestamp = Math.floor(Date.now() / 1000);
79
+ sqlite
80
+ .prepare(
81
+ `INSERT INTO site_member (site_id, user_id, role, created_at, updated_at)
82
+ VALUES (?, ?, 'owner', ?, ?)`,
83
+ )
84
+ .run(siteId, userId, timestamp, timestamp);
85
+ }
86
+
87
+ function insertInstallation(
88
+ sqlite: ReturnType<typeof createTestApp>["sqlite"],
89
+ installationId: string,
90
+ siteId: string,
91
+ login: string,
92
+ ) {
93
+ sqlite
94
+ .prepare(
95
+ `INSERT INTO github_app_installation
96
+ (installation_id, site_id, account_login, account_type, account_avatar_url, added_at)
97
+ VALUES (?, ?, ?, 'User', '', ?)`,
98
+ )
99
+ .run(installationId, siteId, login, Math.floor(Date.now() / 1000));
100
+ }
101
+
102
+ /** An installation on a site belonging to somebody else entirely. */
103
+ function insertOtherTenantInstallation(
104
+ sqlite: ReturnType<typeof createTestApp>["sqlite"],
105
+ ) {
106
+ insertSite(sqlite, OTHER_TENANT_SITE_ID, "other-tenant");
107
+ insertMember(sqlite, OTHER_TENANT_SITE_ID, "somebody-else");
108
+ insertInstallation(sqlite, "inst-theirs", OTHER_TENANT_SITE_ID, "stranger");
109
+ }
110
+
111
+ /**
112
+ * The author's *other* site already connected through the App, and the
113
+ * site under test has no binding of its own — the second-blog case.
114
+ */
115
+ function insertSiblingSiteInstallation(
116
+ sqlite: ReturnType<typeof createTestApp>["sqlite"],
117
+ ) {
118
+ insertSite(sqlite, SECOND_SITE_ID, "second");
119
+ insertMember(sqlite, SECOND_SITE_ID, AUTHOR_ID);
120
+ insertInstallation(sqlite, "inst-mine", SECOND_SITE_ID, "octo");
121
+ }
122
+
123
+ describe("GitHub App connect flow", () => {
124
+ beforeEach(() => {
125
+ vi.clearAllMocks();
126
+ });
127
+
128
+ describe("GET /settings/github-sync/app/install", () => {
129
+ it("sends a first-time author to GitHub to install the App", async () => {
130
+ const { app } = createGitHubApp();
131
+
132
+ const res = await app.request("/settings/github-sync/app/install");
133
+
134
+ expect(res.status).toBe(302);
135
+ expect(res.headers.get("location")).toContain(
136
+ "https://github.com/apps/jant-sync/installations/new",
137
+ );
138
+ });
139
+
140
+ it("renders the picker when the author authorized an account on another of their sites", async () => {
141
+ const { app, sqlite } = createGitHubApp();
142
+ insertSiblingSiteInstallation(sqlite);
143
+
144
+ const res = await app.request("/settings/github-sync/app/install");
145
+
146
+ expect(res.status).toBe(200);
147
+ expect(await res.text()).toContain("<jant-repo-picker");
148
+ });
149
+
150
+ it("still reaches GitHub when the author asks to add another account", async () => {
151
+ const { app, sqlite } = createGitHubApp();
152
+ insertSiblingSiteInstallation(sqlite);
153
+
154
+ const res = await app.request(
155
+ "/settings/github-sync/app/install?force=new",
156
+ );
157
+
158
+ expect(res.status).toBe(302);
159
+ expect(res.headers.get("location")).toContain("github.com");
160
+ });
161
+
162
+ it("ignores an installation bound to another tenant's site", async () => {
163
+ const { app, sqlite } = createGitHubApp();
164
+ insertOtherTenantInstallation(sqlite);
165
+
166
+ const res = await app.request("/settings/github-sync/app/install");
167
+
168
+ expect(res.status).toBe(302);
169
+ });
170
+ });
171
+
172
+ describe("the labels the picker is handed", () => {
173
+ async function renderPicker() {
174
+ const { app, sqlite } = createGitHubApp();
175
+ insertSiblingSiteInstallation(sqlite);
176
+ const res = await app.request("/settings/github-sync/app/install");
177
+ expect(res.status).toBe(200);
178
+ return await res.text();
179
+ }
180
+
181
+ it("fills in the suggested repository name", async () => {
182
+ // `i18n._()` renders a placeholder it has no value for as an empty
183
+ // string, which left the hint reading "Name prefilled as ."
184
+ expect(await renderPicker()).toMatch(
185
+ /Name prefilled as [a-z0-9-]+-jant-sync\./,
186
+ );
187
+ });
188
+
189
+ it("keeps the placeholders the component fills in itself", async () => {
190
+ const html = await renderPicker();
191
+
192
+ expect(html).toContain("Showing {shown} of {total}");
193
+ expect(html).toContain("Type {repo} to confirm");
194
+ });
195
+ });
196
+
197
+ describe("GET /settings/github-sync/app/installations", () => {
198
+ it("lists the account the author authorized on another of their sites", async () => {
199
+ const { app, sqlite } = createGitHubApp();
200
+ insertSiblingSiteInstallation(sqlite);
201
+ insertOtherTenantInstallation(sqlite);
202
+
203
+ const res = await app.request("/settings/github-sync/app/installations");
204
+
205
+ expect(res.status).toBe(200);
206
+ const body = (await res.json()) as {
207
+ installations: Array<{
208
+ installationId: string;
209
+ account: { login: string };
210
+ }>;
211
+ };
212
+ expect(body.installations).toHaveLength(1);
213
+ expect(body.installations[0]).toMatchObject({
214
+ installationId: "inst-mine",
215
+ account: { login: "octo" },
216
+ });
217
+ });
218
+ });
219
+
220
+ describe("another tenant's installation id", () => {
221
+ it("is not a repository list", async () => {
222
+ const { app, sqlite } = createGitHubApp();
223
+ insertOtherTenantInstallation(sqlite);
224
+
225
+ const res = await app.request(
226
+ "/settings/github-sync/app/repos?installationId=inst-theirs",
227
+ );
228
+
229
+ expect(res.status).toBe(404);
230
+ });
231
+
232
+ it("is not classifiable", async () => {
233
+ const { app, sqlite } = createGitHubApp();
234
+ insertOtherTenantInstallation(sqlite);
235
+
236
+ const res = await app.request("/settings/github-sync/app/classify", {
237
+ method: "POST",
238
+ headers: { "content-type": "application/json" },
239
+ body: JSON.stringify({
240
+ installationId: "inst-theirs",
241
+ repo: "stranger/private-notes",
242
+ }),
243
+ });
244
+
245
+ expect(res.status).toBe(404);
246
+ expect(classifyRepoForSync).not.toHaveBeenCalled();
247
+ });
248
+
249
+ it("is not connectable", async () => {
250
+ const { app, sqlite } = createGitHubApp();
251
+ insertOtherTenantInstallation(sqlite);
252
+
253
+ const res = await app.request("/settings/github-sync/app/connect", {
254
+ method: "POST",
255
+ headers: { "content-type": "application/json" },
256
+ body: JSON.stringify({
257
+ installationId: "inst-theirs",
258
+ repo: "stranger/private-notes",
259
+ confirmForeign: true,
260
+ }),
261
+ });
262
+
263
+ expect(res.status).toBe(404);
264
+ expect(setupWebhook).not.toHaveBeenCalled();
265
+ });
266
+ });
267
+
268
+ describe("POST /settings/github-sync/app/connect", () => {
269
+ it("records the installation against this site", async () => {
270
+ const { app, sqlite, services } = createGitHubApp();
271
+ insertSiblingSiteInstallation(sqlite);
272
+ classifyRepoForSync.mockResolvedValue({ kind: "empty" });
273
+
274
+ const res = await app.request("/settings/github-sync/app/connect", {
275
+ method: "POST",
276
+ headers: { "content-type": "application/json" },
277
+ body: JSON.stringify({
278
+ installationId: "inst-mine",
279
+ repo: "octo/second-blog",
280
+ }),
281
+ });
282
+
283
+ expect(res.status).toBe(200);
284
+ expect(setupWebhook).toHaveBeenCalledOnce();
285
+
286
+ // Without this row the App webhook's fan-out — uninstall, suspend,
287
+ // repository removed — never reaches the site it just connected.
288
+ const bound =
289
+ await services.githubAppInstallations.listInstallationsForSite(
290
+ DEFAULT_TEST_SITE_ID,
291
+ );
292
+ expect(bound).toHaveLength(1);
293
+ expect(bound[0]).toMatchObject({
294
+ installationId: "inst-mine",
295
+ account: { login: "octo" },
296
+ });
297
+ expect(await services.settings.get("GITHUB_SYNC_REPO")).toBe(
298
+ "octo/second-blog",
299
+ );
300
+ expect(await services.settings.get("GITHUB_SYNC_ENABLED")).toBe("true");
301
+ });
302
+ });
303
+ describe("a site syncing through the App with no binding to show for it", () => {
304
+ /** What the old connect path left behind: settings, no junction row. */
305
+ function insertUnboundSyncingSite(
306
+ sqlite: ReturnType<typeof createTestApp>["sqlite"],
307
+ ) {
308
+ insertSite(sqlite, SECOND_SITE_ID, "second");
309
+ insertMember(sqlite, SECOND_SITE_ID, AUTHOR_ID);
310
+ const timestamp = Math.floor(Date.now() / 1000);
311
+ for (const [key, value] of [
312
+ ["GITHUB_SYNC_AUTH_MODE", "app"],
313
+ ["GITHUB_SYNC_APP_INSTALLATION_ID", "inst-orphan"],
314
+ ]) {
315
+ sqlite
316
+ .prepare(
317
+ `INSERT INTO site_setting (site_id, key, value, updated_at)
318
+ VALUES (?, ?, ?, ?)`,
319
+ )
320
+ .run(SECOND_SITE_ID, key, value, timestamp);
321
+ }
322
+ }
323
+
324
+ it("rebuilds the binding from GitHub instead of sending the author to a dead end", async () => {
325
+ const { app, sqlite, services } = createGitHubApp();
326
+ insertUnboundSyncingSite(sqlite);
327
+ getInstallation.mockResolvedValue({
328
+ account: { login: "octo", type: "User", avatarUrl: "" },
329
+ });
330
+
331
+ const res = await app.request("/settings/github-sync/app/install");
332
+
333
+ expect(res.status).toBe(200);
334
+ expect(await res.text()).toContain("<jant-repo-picker");
335
+ expect(getInstallation).toHaveBeenCalledOnce();
336
+ expect(
337
+ await services.githubAppInstallations.listInstallationsForSite(
338
+ SECOND_SITE_ID,
339
+ ),
340
+ ).toMatchObject([{ installationId: "inst-orphan" }]);
341
+ });
342
+
343
+ it("falls back to the GitHub redirect when the installation is gone", async () => {
344
+ const { app, sqlite } = createGitHubApp();
345
+ insertUnboundSyncingSite(sqlite);
346
+ getInstallation.mockRejectedValue(
347
+ new Error("Fetching installation failed (404)"),
348
+ );
349
+
350
+ const res = await app.request("/settings/github-sync/app/install");
351
+
352
+ expect(res.status).toBe(302);
353
+ });
354
+
355
+ it("does not touch GitHub when the author already has a binding", async () => {
356
+ const { app, sqlite } = createGitHubApp();
357
+ insertSiblingSiteInstallation(sqlite);
358
+
359
+ const res = await app.request("/settings/github-sync/app/install");
360
+
361
+ expect(res.status).toBe(200);
362
+ expect(getInstallation).not.toHaveBeenCalled();
363
+ });
364
+ });
365
+ });