@jant/core 0.3.35 → 0.3.37

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 (307) hide show
  1. package/bin/commands/export.js +1 -1
  2. package/bin/commands/import-site.js +529 -0
  3. package/bin/commands/reset-password.js +3 -2
  4. package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
  5. package/dist/client/assets/module-RjUF93sV.js +716 -0
  6. package/dist/client/assets/native-48B9X9Wg.js +1 -0
  7. package/dist/client/assets/url-FWFqPJPb.js +1 -0
  8. package/dist/client/client.css +1 -1
  9. package/dist/client/client.js +4564 -3013
  10. package/dist/index.js +12885 -8161
  11. package/package.json +23 -6
  12. package/src/__tests__/helpers/app.ts +10 -10
  13. package/src/__tests__/helpers/db.ts +91 -87
  14. package/src/app.tsx +157 -31
  15. package/src/auth.ts +20 -2
  16. package/src/client/archive-nav.js +187 -0
  17. package/src/client/audio-player.ts +478 -0
  18. package/src/client/audio-processor.ts +84 -0
  19. package/src/{lib → client}/avatar-upload.ts +4 -3
  20. package/src/{lib → client}/collection-form-bridge.ts +2 -2
  21. package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
  22. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
  23. package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
  24. package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
  25. package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
  26. package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
  27. package/src/client/components/collection-sidebar-types.ts +43 -0
  28. package/src/{ui → client}/components/collection-types.ts +3 -4
  29. package/src/client/components/compose-types.ts +174 -0
  30. package/src/client/components/jant-collection-form.ts +667 -0
  31. package/src/client/components/jant-collection-sidebar.ts +805 -0
  32. package/src/client/components/jant-compose-dialog.ts +2161 -0
  33. package/src/client/components/jant-compose-editor.ts +1813 -0
  34. package/src/client/components/jant-compose-fullscreen.ts +283 -0
  35. package/src/client/components/jant-media-lightbox.ts +259 -0
  36. package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
  37. package/src/{ui → client}/components/jant-post-form.ts +141 -12
  38. package/src/client/components/jant-post-menu.ts +1019 -0
  39. package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
  40. package/src/{ui → client}/components/jant-settings-general.ts +38 -4
  41. package/src/client/components/jant-text-preview.ts +232 -0
  42. package/src/{ui → client}/components/nav-manager-types.ts +6 -18
  43. package/src/{ui → client}/components/post-form-template.ts +137 -38
  44. package/src/{ui → client}/components/post-form-types.ts +15 -4
  45. package/src/client/compose-bridge.ts +583 -0
  46. package/src/{lib → client}/image-processor.ts +26 -8
  47. package/src/client/lazy-slugify.ts +51 -0
  48. package/src/client/media-metadata.ts +247 -0
  49. package/src/client/multipart-upload.ts +160 -0
  50. package/src/{lib → client}/nav-manager-bridge.ts +1 -1
  51. package/src/{lib → client}/post-form-bridge.ts +53 -2
  52. package/src/{lib → client}/settings-bridge.ts +3 -15
  53. package/src/client/thread-context.ts +140 -0
  54. package/src/client/tiptap/bubble-menu.ts +205 -0
  55. package/src/client/tiptap/create-editor.ts +86 -0
  56. package/src/client/tiptap/exitable-marks.ts +73 -0
  57. package/src/client/tiptap/extensions.ts +65 -0
  58. package/src/client/tiptap/image-node.ts +482 -0
  59. package/src/client/tiptap/link-toolbar.ts +371 -0
  60. package/src/client/tiptap/more-break.ts +50 -0
  61. package/src/client/tiptap/paste-image.ts +129 -0
  62. package/src/client/tiptap/slash-commands.ts +438 -0
  63. package/src/{lib → client}/toast.ts +101 -3
  64. package/src/client/types/sortablejs.d.ts +44 -0
  65. package/src/client/upload-with-metadata.ts +54 -0
  66. package/src/client/video-processor.ts +207 -0
  67. package/src/client.ts +27 -17
  68. package/src/db/__tests__/migrations.test.ts +118 -0
  69. package/src/db/index.ts +52 -0
  70. package/src/db/migrations/0000_baseline.sql +269 -0
  71. package/src/db/migrations/0001_fts_setup.sql +31 -0
  72. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  73. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  74. package/src/db/migrations/meta/_journal.json +4 -39
  75. package/src/db/schema.ts +409 -140
  76. package/src/i18n/__tests__/detect.test.ts +115 -0
  77. package/src/i18n/context.tsx +2 -2
  78. package/src/i18n/detect.ts +85 -1
  79. package/src/i18n/i18n.ts +1 -1
  80. package/src/i18n/index.ts +2 -1
  81. package/src/i18n/locales/en.po +783 -1087
  82. package/src/i18n/locales/en.ts +1 -1
  83. package/src/i18n/locales/zh-Hans.po +867 -812
  84. package/src/i18n/locales/zh-Hans.ts +1 -1
  85. package/src/i18n/locales/zh-Hant.po +878 -823
  86. package/src/i18n/locales/zh-Hant.ts +1 -1
  87. package/src/i18n/middleware.ts +6 -0
  88. package/src/index.ts +5 -7
  89. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  90. package/src/lib/__tests__/constants.test.ts +0 -1
  91. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  92. package/src/lib/__tests__/nanoid.test.ts +26 -0
  93. package/src/lib/__tests__/resolve-config.test.ts +2 -2
  94. package/src/lib/__tests__/schemas.test.ts +186 -65
  95. package/src/lib/__tests__/slug.test.ts +126 -0
  96. package/src/lib/__tests__/sse.test.ts +6 -6
  97. package/src/lib/__tests__/summary.test.ts +264 -0
  98. package/src/lib/__tests__/theme.test.ts +1 -1
  99. package/src/lib/__tests__/timeline.test.ts +33 -30
  100. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  101. package/src/lib/__tests__/url.test.ts +2 -2
  102. package/src/lib/__tests__/view.test.ts +140 -65
  103. package/src/lib/blurhash-placeholder.ts +102 -0
  104. package/src/lib/constants.ts +3 -1
  105. package/src/lib/emoji-catalog.ts +963 -0
  106. package/src/lib/errors.ts +11 -8
  107. package/src/lib/feed.ts +77 -31
  108. package/src/lib/html.ts +2 -1
  109. package/src/lib/icon-catalog.ts +5033 -1
  110. package/src/lib/icons.ts +3 -2
  111. package/src/lib/index.ts +0 -1
  112. package/src/lib/markdown-to-tiptap.ts +286 -0
  113. package/src/lib/media-helpers.ts +22 -12
  114. package/src/lib/nanoid.ts +29 -0
  115. package/src/lib/navigation.ts +1 -1
  116. package/src/lib/render.tsx +24 -5
  117. package/src/lib/resolve-config.ts +13 -2
  118. package/src/lib/schemas.ts +226 -58
  119. package/src/lib/search-snippet.ts +34 -0
  120. package/src/lib/slug.ts +96 -0
  121. package/src/lib/sse.ts +6 -6
  122. package/src/lib/storage.ts +115 -7
  123. package/src/lib/summary.ts +158 -0
  124. package/src/lib/theme.ts +11 -8
  125. package/src/lib/timeline.ts +76 -34
  126. package/src/lib/tiptap-render.ts +191 -0
  127. package/src/lib/tiptap-to-markdown.ts +305 -0
  128. package/src/lib/upload.ts +263 -14
  129. package/src/lib/url.ts +37 -22
  130. package/src/lib/view.ts +236 -55
  131. package/src/middleware/__tests__/auth.test.ts +191 -11
  132. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  133. package/src/middleware/auth.ts +63 -9
  134. package/src/middleware/error-handler.ts +3 -3
  135. package/src/middleware/onboarding.ts +1 -1
  136. package/src/middleware/secure-headers.ts +40 -0
  137. package/src/preset.css +83 -2
  138. package/src/routes/__tests__/compose.test.ts +17 -24
  139. package/src/routes/api/__tests__/collections.test.ts +109 -61
  140. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  141. package/src/routes/api/__tests__/posts.test.ts +132 -68
  142. package/src/routes/api/__tests__/search.test.ts +15 -2
  143. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  144. package/src/routes/api/collections.ts +57 -31
  145. package/src/routes/api/custom-urls.ts +80 -0
  146. package/src/routes/api/export.ts +31 -0
  147. package/src/routes/api/nav-items.ts +23 -19
  148. package/src/routes/api/posts.ts +81 -62
  149. package/src/routes/api/search.ts +3 -4
  150. package/src/routes/api/upload-multipart.ts +245 -0
  151. package/src/routes/api/upload.ts +92 -24
  152. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  153. package/src/routes/auth/reset.tsx +5 -4
  154. package/src/routes/auth/setup.tsx +39 -31
  155. package/src/routes/auth/signin.tsx +13 -14
  156. package/src/routes/compose.tsx +27 -63
  157. package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
  158. package/src/routes/dash/custom-urls.tsx +414 -0
  159. package/src/routes/dash/settings.tsx +475 -99
  160. package/src/routes/feed/__tests__/rss.test.ts +22 -23
  161. package/src/routes/feed/rss.ts +6 -2
  162. package/src/routes/feed/sitemap.ts +2 -12
  163. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  164. package/src/routes/pages/__tests__/featured.test.ts +36 -18
  165. package/src/routes/pages/archive.tsx +177 -37
  166. package/src/routes/pages/collection.tsx +43 -14
  167. package/src/routes/pages/collections.tsx +11 -2
  168. package/src/routes/pages/featured.tsx +27 -3
  169. package/src/routes/pages/home.tsx +15 -14
  170. package/src/routes/pages/latest.tsx +1 -11
  171. package/src/routes/pages/new.tsx +39 -0
  172. package/src/routes/pages/page.tsx +95 -49
  173. package/src/routes/pages/search.tsx +1 -1
  174. package/src/services/__tests__/api-token.test.ts +135 -0
  175. package/src/services/__tests__/collection.test.ts +275 -227
  176. package/src/services/__tests__/custom-url.test.ts +213 -0
  177. package/src/services/__tests__/media.test.ts +162 -22
  178. package/src/services/__tests__/navigation.test.ts +109 -68
  179. package/src/services/__tests__/post-timeline.test.ts +205 -32
  180. package/src/services/__tests__/post.test.ts +800 -230
  181. package/src/services/__tests__/search.test.ts +67 -10
  182. package/src/services/__tests__/settings.test.ts +3 -3
  183. package/src/services/api-token.ts +166 -0
  184. package/src/services/auth.ts +17 -2
  185. package/src/services/collection.ts +397 -131
  186. package/src/services/custom-url.ts +188 -0
  187. package/src/services/export.ts +802 -0
  188. package/src/services/index.ts +26 -19
  189. package/src/services/media.ts +100 -22
  190. package/src/services/navigation.ts +158 -47
  191. package/src/services/path.ts +339 -0
  192. package/src/services/post.ts +764 -172
  193. package/src/services/search.ts +161 -74
  194. package/src/services/settings.ts +6 -2
  195. package/src/styles/components.css +293 -62
  196. package/src/styles/tokens.css +93 -5
  197. package/src/styles/ui.css +4349 -766
  198. package/src/types/bindings.ts +8 -0
  199. package/src/types/config.ts +34 -4
  200. package/src/types/constants.ts +17 -2
  201. package/src/types/entities.ts +83 -37
  202. package/src/types/operations.ts +20 -27
  203. package/src/types/props.ts +52 -17
  204. package/src/types/views.ts +48 -24
  205. package/src/ui/color-themes.ts +133 -23
  206. package/src/ui/compose/ComposeDialog.tsx +255 -16
  207. package/src/ui/compose/ComposePrompt.tsx +1 -1
  208. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  209. package/src/ui/dash/ListItemRow.tsx +1 -1
  210. package/src/ui/dash/StatusBadge.tsx +12 -2
  211. package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
  212. package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
  213. package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
  214. package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
  215. package/src/ui/dash/index.ts +0 -3
  216. package/src/ui/dash/settings/AccountContent.tsx +87 -146
  217. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  218. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  219. package/src/ui/dash/settings/AvatarContent.tsx +78 -0
  220. package/src/ui/dash/settings/GeneralContent.tsx +3 -62
  221. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  222. package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
  223. package/src/ui/feed/LinkCard.tsx +89 -40
  224. package/src/ui/feed/NoteCard.tsx +39 -25
  225. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  226. package/src/ui/feed/QuoteCard.tsx +38 -23
  227. package/src/ui/feed/ThreadPreview.tsx +90 -26
  228. package/src/ui/feed/TimelineFeed.tsx +3 -2
  229. package/src/ui/feed/TimelineItem.tsx +15 -6
  230. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  231. package/src/ui/feed/thread-preview-state.ts +61 -0
  232. package/src/ui/font-themes.ts +2 -2
  233. package/src/ui/layouts/BaseLayout.tsx +2 -2
  234. package/src/ui/layouts/SiteLayout.tsx +116 -103
  235. package/src/ui/pages/ArchivePage.tsx +923 -95
  236. package/src/ui/pages/CollectionPage.tsx +6 -35
  237. package/src/ui/pages/CollectionsPage.tsx +2 -1
  238. package/src/ui/pages/ComposePage.tsx +54 -0
  239. package/src/ui/pages/FeaturedPage.tsx +2 -1
  240. package/src/ui/pages/HomePage.tsx +1 -1
  241. package/src/ui/pages/PostPage.tsx +30 -45
  242. package/src/ui/pages/SearchPage.tsx +182 -38
  243. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  244. package/src/ui/shared/CollectionsSidebar.tsx +239 -4
  245. package/src/ui/shared/MediaGallery.tsx +475 -41
  246. package/src/ui/shared/PostFooter.tsx +204 -0
  247. package/src/ui/shared/StarRating.tsx +27 -0
  248. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  249. package/src/ui/shared/index.ts +0 -1
  250. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  251. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  252. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  253. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  254. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  255. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  256. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  257. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  258. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  259. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  260. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  261. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  262. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  263. package/src/lib/__tests__/sqid.test.ts +0 -65
  264. package/src/lib/collections-reorder.ts +0 -28
  265. package/src/lib/compose-bridge.ts +0 -280
  266. package/src/lib/media-upload.ts +0 -148
  267. package/src/lib/sqid.ts +0 -79
  268. package/src/routes/api/__tests__/pages.test.ts +0 -218
  269. package/src/routes/api/pages.ts +0 -73
  270. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  271. package/src/routes/dash/appearance.tsx +0 -240
  272. package/src/routes/dash/collections.tsx +0 -211
  273. package/src/routes/dash/index.tsx +0 -103
  274. package/src/routes/dash/media.tsx +0 -132
  275. package/src/routes/dash/pages.tsx +0 -239
  276. package/src/routes/dash/posts.tsx +0 -334
  277. package/src/routes/dash/redirects.tsx +0 -257
  278. package/src/routes/pages/post.tsx +0 -59
  279. package/src/services/__tests__/page.test.ts +0 -298
  280. package/src/services/__tests__/path-registry.test.ts +0 -165
  281. package/src/services/__tests__/redirect.test.ts +0 -159
  282. package/src/services/page.ts +0 -203
  283. package/src/services/path-registry.ts +0 -160
  284. package/src/services/redirect.ts +0 -97
  285. package/src/types/sortablejs.d.ts +0 -29
  286. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
  287. package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
  288. package/src/ui/components/compose-types.ts +0 -75
  289. package/src/ui/components/jant-collection-form.ts +0 -512
  290. package/src/ui/components/jant-compose-dialog.ts +0 -495
  291. package/src/ui/components/jant-compose-editor.ts +0 -814
  292. package/src/ui/dash/PageForm.tsx +0 -185
  293. package/src/ui/dash/PostList.tsx +0 -95
  294. package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
  295. package/src/ui/dash/collections/CollectionForm.tsx +0 -166
  296. package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
  297. package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
  298. package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
  299. package/src/ui/dash/media/MediaListContent.tsx +0 -201
  300. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  301. package/src/ui/dash/pages/PagesContent.tsx +0 -74
  302. package/src/ui/dash/posts/PostForm.tsx +0 -248
  303. package/src/ui/dash/settings/SettingsNav.tsx +0 -52
  304. package/src/ui/layouts/DashLayout.tsx +0 -165
  305. package/src/ui/pages/SinglePage.tsx +0 -23
  306. package/src/ui/shared/ThreadView.tsx +0 -136
  307. /package/src/{ui → client}/components/settings-types.ts +0 -0
@@ -1,6 +1,6 @@
1
- import { describe, it, expect } from "vitest";
1
+ import { describe, it, expect, vi } from "vitest";
2
2
  import { Hono } from "hono";
3
- import { requireAuth, requireAuthApi } from "../auth.js";
3
+ import { requireAuth, requireAuthApi, isLocalHostname } from "../auth.js";
4
4
  import { errorHandler } from "../error-handler.js";
5
5
  import type { Bindings } from "../../types.js";
6
6
  import type { AppVariables } from "../../types/app-context.js";
@@ -21,6 +21,32 @@ function createMockAuth(authenticated: boolean) {
21
21
  } as AppVariables["auth"];
22
22
  }
23
23
 
24
+ function createMockApiTokenService(validToken?: string) {
25
+ const tokenId = "token-id-1";
26
+ return {
27
+ verify: vi.fn(async (raw: string) => (raw === validToken ? tokenId : null)),
28
+ updateLastUsed: vi.fn(async () => {}),
29
+ create: vi.fn(),
30
+ list: vi.fn(),
31
+ delete: vi.fn(),
32
+ };
33
+ }
34
+
35
+ describe("isLocalHostname", () => {
36
+ it.each([
37
+ ["localhost", true],
38
+ ["127.0.0.1", true],
39
+ ["::1", true],
40
+ ["jant.localtest.me", true],
41
+ ["sub.localtest.me", true],
42
+ ["myblog.com", false],
43
+ ["demo.jant.me", false],
44
+ ["localtest.me.evil.com", false],
45
+ ])("isLocalHostname(%s) → %s", (hostname, expected) => {
46
+ expect(isLocalHostname(hostname)).toBe(expected);
47
+ });
48
+ });
49
+
24
50
  describe("requireAuth", () => {
25
51
  it("allows authenticated requests", async () => {
26
52
  const app = new Hono<Env>();
@@ -28,11 +54,11 @@ describe("requireAuth", () => {
28
54
  c.set("auth", createMockAuth(true));
29
55
  await next();
30
56
  });
31
- app.get("/dash", requireAuth(), (c) => c.text("Dashboard"));
57
+ app.get("/settings", requireAuth(), (c) => c.text("Settings"));
32
58
 
33
- const res = await app.request("/dash");
59
+ const res = await app.request("/settings");
34
60
  expect(res.status).toBe(200);
35
- expect(await res.text()).toBe("Dashboard");
61
+ expect(await res.text()).toBe("Settings");
36
62
  });
37
63
 
38
64
  it("redirects unauthenticated requests to /signin", async () => {
@@ -41,9 +67,9 @@ describe("requireAuth", () => {
41
67
  c.set("auth", createMockAuth(false));
42
68
  await next();
43
69
  });
44
- app.get("/dash", requireAuth(), (c) => c.text("Dashboard"));
70
+ app.get("/settings", requireAuth(), (c) => c.text("Settings"));
45
71
 
46
- const res = await app.request("/dash", { redirect: "manual" });
72
+ const res = await app.request("/settings", { redirect: "manual" });
47
73
  expect(res.status).toBe(302);
48
74
  expect(res.headers.get("Location")).toBe("/signin");
49
75
  });
@@ -54,20 +80,23 @@ describe("requireAuth", () => {
54
80
  c.set("auth", createMockAuth(false));
55
81
  await next();
56
82
  });
57
- app.get("/dash", requireAuth("/login"), (c) => c.text("Dashboard"));
83
+ app.get("/settings", requireAuth("/login"), (c) => c.text("Settings"));
58
84
 
59
- const res = await app.request("/dash", { redirect: "manual" });
85
+ const res = await app.request("/settings", { redirect: "manual" });
60
86
  expect(res.status).toBe(302);
61
87
  expect(res.headers.get("Location")).toBe("/login");
62
88
  });
63
89
  });
64
90
 
65
91
  describe("requireAuthApi", () => {
66
- it("allows authenticated requests", async () => {
92
+ it("allows authenticated requests via session", async () => {
67
93
  const app = new Hono<Env>();
68
94
  app.onError(errorHandler);
69
95
  app.use("*", async (c, next) => {
70
96
  c.set("auth", createMockAuth(true));
97
+ c.set("services", {
98
+ apiTokens: createMockApiTokenService(),
99
+ } as AppVariables["services"]);
71
100
  await next();
72
101
  });
73
102
  app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
@@ -79,11 +108,14 @@ describe("requireAuthApi", () => {
79
108
  expect(body.data).toBe("secret");
80
109
  });
81
110
 
82
- it("returns 401 for unauthenticated requests", async () => {
111
+ it("returns 401 for unauthenticated requests without Bearer token", async () => {
83
112
  const app = new Hono<Env>();
84
113
  app.onError(errorHandler);
85
114
  app.use("*", async (c, next) => {
86
115
  c.set("auth", createMockAuth(false));
116
+ c.set("services", {
117
+ apiTokens: createMockApiTokenService(),
118
+ } as AppVariables["services"]);
87
119
  await next();
88
120
  });
89
121
  app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
@@ -107,6 +139,9 @@ describe("requireAuthApi", () => {
107
139
  },
108
140
  },
109
141
  } as AppVariables["auth"]);
142
+ c.set("services", {
143
+ apiTokens: createMockApiTokenService(),
144
+ } as AppVariables["services"]);
110
145
  await next();
111
146
  });
112
147
  app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
@@ -114,4 +149,149 @@ describe("requireAuthApi", () => {
114
149
  const res = await app.request("/api/data");
115
150
  expect(res.status).toBe(401);
116
151
  });
152
+
153
+ it("allows requests with valid Bearer token when session auth fails", async () => {
154
+ const validToken = "jnt_abc123";
155
+ const mockApiTokens = createMockApiTokenService(validToken);
156
+
157
+ const app = new Hono<Env>();
158
+ app.onError(errorHandler);
159
+ app.use("*", async (c, next) => {
160
+ c.set("auth", createMockAuth(false));
161
+ c.set("services", {
162
+ apiTokens: mockApiTokens,
163
+ } as AppVariables["services"]);
164
+ await next();
165
+ });
166
+ app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
167
+
168
+ const res = await app.request("/api/data", {
169
+ headers: { Authorization: `Bearer ${validToken}` },
170
+ });
171
+ expect(res.status).toBe(200);
172
+
173
+ const body = await res.json();
174
+ expect(body.data).toBe("secret");
175
+
176
+ expect(mockApiTokens.verify).toHaveBeenCalledWith(validToken);
177
+ expect(mockApiTokens.updateLastUsed).toHaveBeenCalledWith("token-id-1");
178
+ });
179
+
180
+ it("returns 401 for invalid Bearer token", async () => {
181
+ const mockApiTokens = createMockApiTokenService("jnt_valid");
182
+
183
+ const app = new Hono<Env>();
184
+ app.onError(errorHandler);
185
+ app.use("*", async (c, next) => {
186
+ c.set("auth", createMockAuth(false));
187
+ c.set("services", {
188
+ apiTokens: mockApiTokens,
189
+ } as AppVariables["services"]);
190
+ await next();
191
+ });
192
+ app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
193
+
194
+ const res = await app.request("/api/data", {
195
+ headers: { Authorization: "Bearer jnt_invalid" },
196
+ });
197
+ expect(res.status).toBe(401);
198
+
199
+ expect(mockApiTokens.verify).toHaveBeenCalledWith("jnt_invalid");
200
+ });
201
+
202
+ it("prefers session auth over Bearer token", async () => {
203
+ const mockApiTokens = createMockApiTokenService("jnt_valid");
204
+
205
+ const app = new Hono<Env>();
206
+ app.onError(errorHandler);
207
+ app.use("*", async (c, next) => {
208
+ c.set("auth", createMockAuth(true));
209
+ c.set("services", {
210
+ apiTokens: mockApiTokens,
211
+ } as AppVariables["services"]);
212
+ await next();
213
+ });
214
+ app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
215
+
216
+ const res = await app.request("/api/data", {
217
+ headers: { Authorization: "Bearer jnt_valid" },
218
+ });
219
+ expect(res.status).toBe(200);
220
+
221
+ // Should not check the token since session auth succeeded
222
+ expect(mockApiTokens.verify).not.toHaveBeenCalled();
223
+ });
224
+
225
+ it("allows DEV_API_TOKEN on localhost", async () => {
226
+ const devToken = "jnt_dev_test123";
227
+ const mockApiTokens = createMockApiTokenService();
228
+
229
+ const app = new Hono<Env>();
230
+ app.onError(errorHandler);
231
+ app.use("*", async (c, next) => {
232
+ c.env = { ...c.env, DEV_API_TOKEN: devToken } as Bindings;
233
+ c.set("auth", createMockAuth(false));
234
+ c.set("services", {
235
+ apiTokens: mockApiTokens,
236
+ } as AppVariables["services"]);
237
+ await next();
238
+ });
239
+ app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
240
+
241
+ const res = await app.request("http://localhost:9020/api/data", {
242
+ headers: { Authorization: `Bearer ${devToken}` },
243
+ });
244
+ expect(res.status).toBe(200);
245
+
246
+ // Should NOT hit DB verification
247
+ expect(mockApiTokens.verify).not.toHaveBeenCalled();
248
+ });
249
+
250
+ it("rejects DEV_API_TOKEN on non-local hostname", async () => {
251
+ const devToken = "jnt_dev_test123";
252
+ const mockApiTokens = createMockApiTokenService();
253
+
254
+ const app = new Hono<Env>();
255
+ app.onError(errorHandler);
256
+ app.use("*", async (c, next) => {
257
+ c.env = { ...c.env, DEV_API_TOKEN: devToken } as Bindings;
258
+ c.set("auth", createMockAuth(false));
259
+ c.set("services", {
260
+ apiTokens: mockApiTokens,
261
+ } as AppVariables["services"]);
262
+ await next();
263
+ });
264
+ app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
265
+
266
+ const res = await app.request("https://myblog.com/api/data", {
267
+ headers: { Authorization: `Bearer ${devToken}` },
268
+ });
269
+ expect(res.status).toBe(401);
270
+
271
+ // Falls through to normal DB verification (which also fails)
272
+ expect(mockApiTokens.verify).toHaveBeenCalledWith(devToken);
273
+ });
274
+
275
+ it("allows DEV_API_TOKEN on *.localtest.me", async () => {
276
+ const devToken = "jnt_dev_test123";
277
+ const mockApiTokens = createMockApiTokenService();
278
+
279
+ const app = new Hono<Env>();
280
+ app.onError(errorHandler);
281
+ app.use("*", async (c, next) => {
282
+ c.env = { ...c.env, DEV_API_TOKEN: devToken } as Bindings;
283
+ c.set("auth", createMockAuth(false));
284
+ c.set("services", {
285
+ apiTokens: mockApiTokens,
286
+ } as AppVariables["services"]);
287
+ await next();
288
+ });
289
+ app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
290
+
291
+ const res = await app.request("https://jant.localtest.me/api/data", {
292
+ headers: { Authorization: `Bearer ${devToken}` },
293
+ });
294
+ expect(res.status).toBe(200);
295
+ expect(mockApiTokens.verify).not.toHaveBeenCalled();
296
+ });
117
297
  });
@@ -31,14 +31,14 @@ function createApp(complete: boolean) {
31
31
 
32
32
  // Register routes for testing
33
33
  app.get("/", (c) => c.text("Home"));
34
- app.get("/dash", (c) => c.text("Dashboard"));
35
- app.get("/dash/posts", (c) => c.text("Posts"));
34
+ app.get("/settings", (c) => c.text("Settings"));
35
+ app.get("/settings/general", (c) => c.text("General"));
36
36
  app.get("/archive", (c) => c.text("Archive"));
37
37
  app.get("/p/abc", (c) => c.text("Post"));
38
38
  app.get("/setup", (c) => c.text("Setup"));
39
39
  app.get("/health", (c) => c.text("OK"));
40
40
  app.get("/signin", (c) => c.text("Signin"));
41
- app.get("/signout", (c) => c.text("Signout"));
41
+ app.post("/signout", (c) => c.text("Signout"));
42
42
  app.get("/reset", (c) => c.text("Reset"));
43
43
  app.get("/api/auth/session", (c) => c.json({ ok: true }));
44
44
  app.get("/assets/client-B2b-1X3C.js", (c) => c.text("js"));
@@ -63,16 +63,18 @@ describe("requireOnboarding", () => {
63
63
  expect(res.headers.get("Location")).toBe("/setup");
64
64
  });
65
65
 
66
- it("redirects /dash to /setup when onboarding not complete", async () => {
66
+ it("redirects /settings to /setup when onboarding not complete", async () => {
67
67
  const { app } = createApp(false);
68
- const res = await app.request("/dash", { redirect: "manual" });
68
+ const res = await app.request("/settings", { redirect: "manual" });
69
69
  expect(res.status).toBe(302);
70
70
  expect(res.headers.get("Location")).toBe("/setup");
71
71
  });
72
72
 
73
- it("redirects /dash/* to /setup when onboarding not complete", async () => {
73
+ it("redirects /settings/* to /setup when onboarding not complete", async () => {
74
74
  const { app } = createApp(false);
75
- const res = await app.request("/dash/posts", { redirect: "manual" });
75
+ const res = await app.request("/settings/general", {
76
+ redirect: "manual",
77
+ });
76
78
  expect(res.status).toBe(302);
77
79
  expect(res.headers.get("Location")).toBe("/setup");
78
80
  });
@@ -105,7 +107,7 @@ describe("requireOnboarding", () => {
105
107
  await app.request("/");
106
108
  expect(getCallCount()).toBe(1);
107
109
 
108
- await app.request("/dash");
110
+ await app.request("/settings");
109
111
  expect(getCallCount()).toBe(1); // still 1 — cached
110
112
  });
111
113
 
@@ -115,7 +117,7 @@ describe("requireOnboarding", () => {
115
117
  await app.request("/", { redirect: "manual" });
116
118
  expect(getCallCount()).toBe(1);
117
119
 
118
- await app.request("/dash", { redirect: "manual" });
120
+ await app.request("/settings", { redirect: "manual" });
119
121
  expect(getCallCount()).toBe(2); // queried again
120
122
  });
121
123
 
@@ -136,7 +138,7 @@ describe("requireOnboarding", () => {
136
138
 
137
139
  it("allows /signout", async () => {
138
140
  const { app, getCallCount } = createApp(false);
139
- const res = await app.request("/signout");
141
+ const res = await app.request("/signout", { method: "POST" });
140
142
  expect(res.status).toBe(200);
141
143
  expect(getCallCount()).toBe(0);
142
144
  });
@@ -1,19 +1,42 @@
1
1
  /**
2
2
  * Authentication Middleware
3
3
  *
4
- * Protects routes by requiring authentication
4
+ * Protects routes by requiring authentication via session cookies
5
+ * or Bearer API tokens.
5
6
  */
6
7
 
7
8
  import type { MiddlewareHandler } from "hono";
8
9
  import type { Bindings } from "../types.js";
9
10
  import type { AppVariables } from "../types/app-context.js";
10
- import { DomainError, UnauthorizedError } from "../lib/errors.js";
11
+ import { UnauthorizedError } from "../lib/errors.js";
11
12
 
12
13
  type Env = { Bindings: Bindings; Variables: AppVariables };
13
14
 
15
+ /**
16
+ * Checks whether a hostname is local (dev environment).
17
+ *
18
+ * @param hostname - The hostname to check
19
+ * @returns `true` for localhost, 127.0.0.1, ::1, and *.localtest.me
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * isLocalHostname("localhost") // true
24
+ * isLocalHostname("myblog.com") // false
25
+ * ```
26
+ */
27
+ export function isLocalHostname(hostname: string): boolean {
28
+ return (
29
+ hostname === "localhost" ||
30
+ hostname === "127.0.0.1" ||
31
+ hostname === "::1" ||
32
+ hostname.endsWith(".localtest.me")
33
+ );
34
+ }
35
+
14
36
  /**
15
37
  * Middleware that requires authentication.
16
38
  * Redirects to signin page if not authenticated.
39
+ * Session-only — Bearer tokens are not accepted for dashboard pages.
17
40
  */
18
41
  export function requireAuth(redirectTo = "/signin"): MiddlewareHandler<Env> {
19
42
  return async (c, next) => {
@@ -35,23 +58,54 @@ export function requireAuth(redirectTo = "/signin"): MiddlewareHandler<Env> {
35
58
 
36
59
  /**
37
60
  * Middleware for API routes that requires authentication.
38
- * Returns 401 if not authenticated.
61
+ * Tries session auth first, then falls back to Bearer API token.
62
+ * Returns 401 if neither method succeeds.
39
63
  */
40
64
  export function requireAuthApi(): MiddlewareHandler<Env> {
41
65
  return async (c, next) => {
66
+ // 1. Try session auth (existing behavior)
42
67
  try {
43
68
  const session = await c.var.auth.api.getSession({
44
69
  headers: c.req.raw.headers,
45
70
  });
46
71
 
47
- if (!session?.user) {
48
- throw new UnauthorizedError();
72
+ if (session?.user) {
73
+ await next();
74
+ return;
49
75
  }
76
+ } catch {
77
+ // Session check failed — fall through to Bearer token
78
+ }
50
79
 
51
- await next();
52
- } catch (err) {
53
- if (err instanceof DomainError) throw err;
54
- throw new UnauthorizedError();
80
+ // 2. Try Bearer token auth
81
+ const authHeader = c.req.header("Authorization");
82
+ if (authHeader?.startsWith("Bearer ")) {
83
+ const rawToken = authHeader.slice(7);
84
+
85
+ // Dev shortcut: bypass DB lookup when DEV_API_TOKEN matches on a local hostname
86
+ const devToken = c.env?.DEV_API_TOKEN;
87
+ if (devToken && rawToken === devToken) {
88
+ const hostname = new URL(c.req.url).hostname;
89
+ if (isLocalHostname(hostname)) {
90
+ await next();
91
+ return;
92
+ }
93
+ }
94
+
95
+ const tokenId = await c.var.services.apiTokens.verify(rawToken);
96
+ if (tokenId) {
97
+ // Fire-and-forget last-used update (non-blocking)
98
+ const updatePromise = c.var.services.apiTokens.updateLastUsed(tokenId);
99
+ try {
100
+ c.executionCtx.waitUntil(updatePromise);
101
+ } catch {
102
+ // executionCtx not available (e.g. in tests) — ignore
103
+ }
104
+ await next();
105
+ return;
106
+ }
55
107
  }
108
+
109
+ throw new UnauthorizedError();
56
110
  };
57
111
  }
@@ -33,7 +33,7 @@ export const errorHandler: ErrorHandler<Env> = (err, c) => {
33
33
  // Unknown API error
34
34
  // eslint-disable-next-line no-console -- Server error logging is intentional
35
35
  console.error("[Jant] Unhandled error:", err);
36
- return c.json({ error: "Internal server error" }, 500);
36
+ return c.json({ error: "Something went wrong on our end" }, 500);
37
37
  }
38
38
 
39
39
  // Datastar requests: return toast
@@ -43,7 +43,7 @@ export const errorHandler: ErrorHandler<Env> = (err, c) => {
43
43
  }
44
44
  // eslint-disable-next-line no-console -- Server error logging is intentional
45
45
  console.error("[Jant] Unhandled error:", err);
46
- return dsToast("An unexpected error occurred", "error");
46
+ return dsToast("Something went wrong. Try refreshing the page.", "error");
47
47
  }
48
48
 
49
49
  // JSON-accepting requests (Lit bridges)
@@ -59,7 +59,7 @@ export const errorHandler: ErrorHandler<Env> = (err, c) => {
59
59
  }
60
60
  // eslint-disable-next-line no-console -- Server error logging is intentional
61
61
  console.error("[Jant] Unhandled error:", err);
62
- return c.json({ error: "Internal server error" }, 500);
62
+ return c.json({ error: "Something went wrong on our end" }, 500);
63
63
  }
64
64
 
65
65
  // Non-API routes: map NotFoundError to Hono's built-in 404
@@ -51,7 +51,7 @@ function shouldRedirect(path: string): boolean {
51
51
  path === "/" ||
52
52
  path === "/signin" ||
53
53
  path === "/reset" ||
54
- path.startsWith("/dash")
54
+ path.startsWith("/settings")
55
55
  );
56
56
  }
57
57
 
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Security Headers Middleware
3
+ *
4
+ * Adds Content-Security-Policy and other security headers via Hono's
5
+ * built-in secureHeaders middleware. Uses a baseline CSP that works with
6
+ * the current tech stack (Datastar, Lit, inline theme styles).
7
+ */
8
+
9
+ import { secureHeaders } from "hono/secure-headers";
10
+ import type { MiddlewareHandler } from "hono";
11
+ import type { Bindings } from "../types.js";
12
+ import type { AppVariables } from "../types/app-context.js";
13
+ import { IS_VITE_DEV } from "../lib/version.js";
14
+
15
+ type Env = { Bindings: Bindings; Variables: AppVariables };
16
+
17
+ export function secureHeadersMiddleware(): MiddlewareHandler<Env> {
18
+ return secureHeaders({
19
+ contentSecurityPolicy: {
20
+ defaultSrc: ["'self'"],
21
+ scriptSrc: [
22
+ "'self'",
23
+ // Datastar evaluates expressions in data-on-* / data-signals attributes
24
+ "'unsafe-eval'",
25
+ ],
26
+ styleSrc: [
27
+ "'self'",
28
+ // Theme styles and custom CSS are injected as inline <style> tags
29
+ "'unsafe-inline'",
30
+ ],
31
+ imgSrc: ["'self'", "data:", "blob:", "https:"],
32
+ fontSrc: ["'self'"],
33
+ connectSrc: IS_VITE_DEV ? ["'self'", "ws:"] : ["'self'"],
34
+ frameSrc: ["'none'"],
35
+ objectSrc: ["'none'"],
36
+ baseUri: ["'self'"],
37
+ formAction: ["'self'"],
38
+ },
39
+ });
40
+ }
package/src/preset.css CHANGED
@@ -13,6 +13,12 @@
13
13
  @import "./styles/tokens.css";
14
14
  @import "./styles/ui.css";
15
15
 
16
+ /*
17
+ * Override BaseCoat's class-based dark mode with media-query-based.
18
+ * Jant follows system preference automatically — no manual toggle.
19
+ */
20
+ @custom-variant dark (@media (prefers-color-scheme: dark));
21
+
16
22
  @theme {
17
23
  --radius-default: 0.5rem;
18
24
  --color-success: var(--success);
@@ -24,8 +30,45 @@
24
30
  --success: oklch(0.518 0.16 145.071);
25
31
  }
26
32
 
27
- .dark {
28
- --success: oklch(0.627 0.194 149.214);
33
+ /*
34
+ * BaseCoat dark mode fallback — mirrors BaseCoat's `.dark { }` variables
35
+ * via media query so dark mode works without JS class toggling.
36
+ * These are overridden by the active color theme (higher specificity).
37
+ *
38
+ * Source: basecoat-css@0.3.11 .dark { } block
39
+ */
40
+ @media (prefers-color-scheme: dark) {
41
+ :root {
42
+ color-scheme: dark;
43
+ --background: oklch(0.145 0 0);
44
+ --foreground: oklch(0.985 0 0);
45
+ --card: oklch(0.205 0 0);
46
+ --card-foreground: oklch(0.985 0 0);
47
+ --popover: oklch(0.269 0 0);
48
+ --popover-foreground: oklch(0.985 0 0);
49
+ --primary: oklch(0.922 0 0);
50
+ --primary-foreground: oklch(0.205 0 0);
51
+ --secondary: oklch(0.269 0 0);
52
+ --secondary-foreground: oklch(0.985 0 0);
53
+ --muted: oklch(0.269 0 0);
54
+ --muted-foreground: oklch(0.708 0 0);
55
+ --accent: oklch(0.371 0 0);
56
+ --accent-foreground: oklch(0.985 0 0);
57
+ --destructive: oklch(0.704 0.191 22.216);
58
+ --border: oklch(1 0 0 / 10%);
59
+ --input: oklch(1 0 0 / 15%);
60
+ --ring: oklch(0.556 0 0);
61
+ --sidebar: oklch(0.205 0 0);
62
+ --sidebar-foreground: oklch(0.985 0 0);
63
+ --sidebar-primary: oklch(0.488 0.243 264.376);
64
+ --sidebar-primary-foreground: oklch(0.985 0 0);
65
+ --sidebar-accent: oklch(0.269 0 0);
66
+ --sidebar-accent-foreground: oklch(0.985 0 0);
67
+ --sidebar-border: oklch(1 0 0 / 10%);
68
+ --sidebar-ring: oklch(0.439 0 0);
69
+ --scrollbar-thumb: rgba(255, 255, 255, 0.3);
70
+ --success: oklch(0.627 0.194 149.214);
71
+ }
29
72
  }
30
73
 
31
74
  /**
@@ -73,4 +116,42 @@
73
116
  :where(h1, h2, h3, h4, h5, h6) {
74
117
  font-family: var(--font-heading);
75
118
  }
119
+
120
+ /* Image figures */
121
+ figure {
122
+ margin: 1.5em 0;
123
+ }
124
+
125
+ figure img {
126
+ width: 100%;
127
+ max-height: 500px;
128
+ object-fit: contain;
129
+ border-radius: 6px;
130
+ cursor: pointer;
131
+ }
132
+
133
+ figcaption {
134
+ text-align: center;
135
+ font-size: 0.875rem;
136
+ color: var(--tw-prose-captions);
137
+ margin-top: 0.5em;
138
+ }
139
+
140
+ /* Layout variants — center-breakout via margin+transform (no scrollbars) */
141
+ figure[data-layout="wide"] {
142
+ width: 1200px;
143
+ max-width: 100vw;
144
+ margin-left: 50%;
145
+ transform: translateX(-50%);
146
+ }
147
+
148
+ figure[data-layout="full"] {
149
+ width: 100vw;
150
+ margin-left: 50%;
151
+ transform: translateX(-50%);
152
+ }
153
+
154
+ figure[data-layout="full"] img {
155
+ border-radius: 0;
156
+ }
76
157
  }