@jant/core 0.3.36 → 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 (271) 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/url-FWFqPJPb.js +1 -0
  6. package/dist/client/client.css +1 -1
  7. package/dist/client/client.js +4012 -3276
  8. package/dist/index.js +10285 -5809
  9. package/package.json +11 -3
  10. package/src/__tests__/helpers/app.ts +9 -9
  11. package/src/__tests__/helpers/db.ts +91 -93
  12. package/src/app.tsx +157 -27
  13. package/src/auth.ts +20 -2
  14. package/src/client/archive-nav.js +187 -0
  15. package/src/client/audio-player.ts +478 -0
  16. package/src/client/audio-processor.ts +84 -0
  17. package/src/client/avatar-upload.ts +3 -2
  18. package/src/client/components/__tests__/jant-compose-dialog.test.ts +645 -49
  19. package/src/client/components/__tests__/jant-compose-editor.test.ts +208 -16
  20. package/src/client/components/__tests__/jant-post-form.test.ts +19 -9
  21. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  22. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  23. package/src/client/components/collection-sidebar-types.ts +7 -9
  24. package/src/client/components/compose-types.ts +101 -4
  25. package/src/client/components/jant-collection-form.ts +43 -7
  26. package/src/client/components/jant-collection-sidebar.ts +88 -84
  27. package/src/client/components/jant-compose-dialog.ts +1655 -219
  28. package/src/client/components/jant-compose-editor.ts +732 -168
  29. package/src/client/components/jant-compose-fullscreen.ts +23 -78
  30. package/src/client/components/jant-media-lightbox.ts +2 -0
  31. package/src/client/components/jant-nav-manager.ts +24 -284
  32. package/src/client/components/jant-post-form.ts +89 -9
  33. package/src/client/components/jant-post-menu.ts +1019 -0
  34. package/src/client/components/jant-settings-avatar.ts +3 -3
  35. package/src/client/components/jant-settings-general.ts +38 -4
  36. package/src/client/components/jant-text-preview.ts +232 -0
  37. package/src/client/components/nav-manager-types.ts +4 -19
  38. package/src/client/components/post-form-template.ts +107 -12
  39. package/src/client/components/post-form-types.ts +11 -4
  40. package/src/client/compose-bridge.ts +410 -109
  41. package/src/client/image-processor.ts +26 -8
  42. package/src/client/media-metadata.ts +247 -0
  43. package/src/client/multipart-upload.ts +160 -0
  44. package/src/client/post-form-bridge.ts +52 -1
  45. package/src/client/settings-bridge.ts +0 -12
  46. package/src/client/thread-context.ts +140 -0
  47. package/src/client/tiptap/create-editor.ts +46 -0
  48. package/src/client/tiptap/extensions.ts +5 -0
  49. package/src/client/tiptap/image-node.ts +2 -8
  50. package/src/client/tiptap/paste-image.ts +2 -13
  51. package/src/client/tiptap/slash-commands.ts +173 -63
  52. package/src/client/toast.ts +101 -3
  53. package/src/client/types/sortablejs.d.ts +15 -0
  54. package/src/client/upload-with-metadata.ts +54 -0
  55. package/src/client/video-processor.ts +207 -0
  56. package/src/client.ts +5 -2
  57. package/src/db/__tests__/migrations.test.ts +118 -0
  58. package/src/db/index.ts +52 -0
  59. package/src/db/migrations/0000_baseline.sql +269 -0
  60. package/src/db/migrations/0001_fts_setup.sql +31 -0
  61. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  62. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  63. package/src/db/migrations/meta/_journal.json +4 -39
  64. package/src/db/schema.ts +409 -145
  65. package/src/i18n/__tests__/detect.test.ts +115 -0
  66. package/src/i18n/context.tsx +2 -2
  67. package/src/i18n/detect.ts +85 -1
  68. package/src/i18n/i18n.ts +1 -1
  69. package/src/i18n/index.ts +2 -1
  70. package/src/i18n/locales/en.po +487 -1217
  71. package/src/i18n/locales/en.ts +1 -1
  72. package/src/i18n/locales/zh-Hans.po +613 -996
  73. package/src/i18n/locales/zh-Hans.ts +1 -1
  74. package/src/i18n/locales/zh-Hant.po +624 -1007
  75. package/src/i18n/locales/zh-Hant.ts +1 -1
  76. package/src/i18n/middleware.ts +6 -0
  77. package/src/index.ts +5 -7
  78. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  79. package/src/lib/__tests__/constants.test.ts +0 -1
  80. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  81. package/src/lib/__tests__/nanoid.test.ts +26 -0
  82. package/src/lib/__tests__/schemas.test.ts +181 -63
  83. package/src/lib/__tests__/slug.test.ts +126 -0
  84. package/src/lib/__tests__/sse.test.ts +6 -6
  85. package/src/lib/__tests__/summary.test.ts +264 -0
  86. package/src/lib/__tests__/theme.test.ts +1 -1
  87. package/src/lib/__tests__/timeline.test.ts +33 -30
  88. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  89. package/src/lib/__tests__/view.test.ts +141 -66
  90. package/src/lib/blurhash-placeholder.ts +102 -0
  91. package/src/lib/constants.ts +3 -1
  92. package/src/lib/emoji-catalog.ts +885 -68
  93. package/src/lib/errors.ts +11 -8
  94. package/src/lib/feed.ts +78 -32
  95. package/src/lib/html.ts +2 -1
  96. package/src/lib/icon-catalog.ts +5033 -1
  97. package/src/lib/icons.ts +3 -2
  98. package/src/lib/index.ts +0 -1
  99. package/src/lib/markdown-to-tiptap.ts +286 -0
  100. package/src/lib/media-helpers.ts +12 -3
  101. package/src/lib/nanoid.ts +29 -0
  102. package/src/lib/navigation.ts +1 -1
  103. package/src/lib/render.tsx +20 -2
  104. package/src/lib/resolve-config.ts +6 -2
  105. package/src/lib/schemas.ts +224 -55
  106. package/src/lib/search-snippet.ts +34 -0
  107. package/src/lib/slug.ts +96 -0
  108. package/src/lib/sse.ts +6 -6
  109. package/src/lib/storage.ts +115 -7
  110. package/src/lib/summary.ts +66 -0
  111. package/src/lib/theme.ts +11 -8
  112. package/src/lib/timeline.ts +74 -34
  113. package/src/lib/tiptap-render.ts +5 -10
  114. package/src/lib/tiptap-to-markdown.ts +305 -0
  115. package/src/lib/upload.ts +190 -29
  116. package/src/lib/url.ts +31 -0
  117. package/src/lib/view.ts +204 -37
  118. package/src/middleware/__tests__/auth.test.ts +191 -11
  119. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  120. package/src/middleware/auth.ts +63 -9
  121. package/src/middleware/onboarding.ts +1 -1
  122. package/src/middleware/secure-headers.ts +40 -0
  123. package/src/preset.css +45 -2
  124. package/src/routes/__tests__/compose.test.ts +17 -24
  125. package/src/routes/api/__tests__/collections.test.ts +109 -61
  126. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  127. package/src/routes/api/__tests__/posts.test.ts +132 -68
  128. package/src/routes/api/__tests__/search.test.ts +15 -2
  129. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  130. package/src/routes/api/collections.ts +51 -42
  131. package/src/routes/api/custom-urls.ts +80 -0
  132. package/src/routes/api/export.ts +31 -0
  133. package/src/routes/api/nav-items.ts +23 -19
  134. package/src/routes/api/posts.ts +43 -39
  135. package/src/routes/api/search.ts +3 -4
  136. package/src/routes/api/upload-multipart.ts +245 -0
  137. package/src/routes/api/upload.ts +85 -19
  138. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  139. package/src/routes/auth/setup.tsx +26 -33
  140. package/src/routes/auth/signin.tsx +3 -7
  141. package/src/routes/compose.tsx +10 -55
  142. package/src/routes/dash/__tests__/settings-avatar.test.ts +1 -1
  143. package/src/routes/dash/custom-urls.tsx +414 -0
  144. package/src/routes/dash/settings.tsx +304 -232
  145. package/src/routes/feed/__tests__/rss.test.ts +27 -28
  146. package/src/routes/feed/rss.ts +6 -4
  147. package/src/routes/feed/sitemap.ts +2 -12
  148. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  149. package/src/routes/pages/__tests__/featured.test.ts +41 -22
  150. package/src/routes/pages/archive.tsx +175 -39
  151. package/src/routes/pages/collection.tsx +22 -10
  152. package/src/routes/pages/collections.tsx +3 -3
  153. package/src/routes/pages/featured.tsx +28 -4
  154. package/src/routes/pages/home.tsx +16 -15
  155. package/src/routes/pages/latest.tsx +1 -11
  156. package/src/routes/pages/new.tsx +39 -0
  157. package/src/routes/pages/page.tsx +95 -49
  158. package/src/routes/pages/search.tsx +1 -1
  159. package/src/services/__tests__/api-token.test.ts +135 -0
  160. package/src/services/__tests__/collection.test.ts +275 -227
  161. package/src/services/__tests__/custom-url.test.ts +213 -0
  162. package/src/services/__tests__/media.test.ts +162 -22
  163. package/src/services/__tests__/navigation.test.ts +109 -68
  164. package/src/services/__tests__/post-timeline.test.ts +205 -32
  165. package/src/services/__tests__/post.test.ts +713 -234
  166. package/src/services/__tests__/search.test.ts +67 -10
  167. package/src/services/api-token.ts +166 -0
  168. package/src/services/auth.ts +17 -2
  169. package/src/services/collection.ts +397 -131
  170. package/src/services/custom-url.ts +188 -0
  171. package/src/services/export.ts +802 -0
  172. package/src/services/index.ts +26 -19
  173. package/src/services/media.ts +100 -22
  174. package/src/services/navigation.ts +158 -47
  175. package/src/services/path.ts +339 -0
  176. package/src/services/post.ts +687 -154
  177. package/src/services/search.ts +160 -75
  178. package/src/styles/components.css +58 -7
  179. package/src/styles/tokens.css +84 -6
  180. package/src/styles/ui.css +2964 -457
  181. package/src/types/bindings.ts +4 -1
  182. package/src/types/config.ts +12 -4
  183. package/src/types/constants.ts +15 -3
  184. package/src/types/entities.ts +74 -35
  185. package/src/types/operations.ts +11 -24
  186. package/src/types/props.ts +51 -16
  187. package/src/types/views.ts +45 -22
  188. package/src/ui/color-themes.ts +133 -23
  189. package/src/ui/compose/ComposeDialog.tsx +239 -17
  190. package/src/ui/compose/ComposePrompt.tsx +1 -1
  191. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  192. package/src/ui/dash/ListItemRow.tsx +1 -1
  193. package/src/ui/dash/StatusBadge.tsx +3 -1
  194. package/src/ui/dash/appearance/AdvancedContent.tsx +22 -1
  195. package/src/ui/dash/appearance/ColorThemeContent.tsx +22 -2
  196. package/src/ui/dash/appearance/FontThemeContent.tsx +1 -1
  197. package/src/ui/dash/appearance/NavigationContent.tsx +5 -45
  198. package/src/ui/dash/index.ts +0 -3
  199. package/src/ui/dash/settings/AccountContent.tsx +3 -57
  200. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  201. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  202. package/src/ui/dash/settings/AvatarContent.tsx +8 -0
  203. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  204. package/src/ui/dash/settings/SettingsRootContent.tsx +45 -15
  205. package/src/ui/feed/LinkCard.tsx +89 -40
  206. package/src/ui/feed/NoteCard.tsx +39 -25
  207. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  208. package/src/ui/feed/QuoteCard.tsx +38 -23
  209. package/src/ui/feed/ThreadPreview.tsx +90 -26
  210. package/src/ui/feed/TimelineFeed.tsx +3 -2
  211. package/src/ui/feed/TimelineItem.tsx +15 -6
  212. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  213. package/src/ui/feed/thread-preview-state.ts +61 -0
  214. package/src/ui/font-themes.ts +2 -2
  215. package/src/ui/layouts/BaseLayout.tsx +2 -2
  216. package/src/ui/layouts/SiteLayout.tsx +105 -92
  217. package/src/ui/pages/ArchivePage.tsx +923 -98
  218. package/src/ui/pages/ComposePage.tsx +54 -0
  219. package/src/ui/pages/PostPage.tsx +30 -45
  220. package/src/ui/pages/SearchPage.tsx +181 -37
  221. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  222. package/src/ui/shared/CollectionsSidebar.tsx +47 -37
  223. package/src/ui/shared/MediaGallery.tsx +445 -149
  224. package/src/ui/shared/PostFooter.tsx +204 -0
  225. package/src/ui/shared/StarRating.tsx +27 -0
  226. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  227. package/src/ui/shared/index.ts +0 -1
  228. package/dist/client/assets/url-8Dj-5CLW.js +0 -1
  229. package/src/client/media-upload.ts +0 -161
  230. package/src/client/page-slug-bridge.ts +0 -42
  231. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  232. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  233. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  234. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  235. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  236. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  237. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  238. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  239. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  240. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  241. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  242. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  243. package/src/db/migrations/0012_add_tiptap_columns.sql +0 -2
  244. package/src/db/migrations/0013_replace_featured_with_visibility.sql +0 -8
  245. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  246. package/src/lib/__tests__/sqid.test.ts +0 -65
  247. package/src/lib/sqid.ts +0 -79
  248. package/src/routes/api/__tests__/pages.test.ts +0 -218
  249. package/src/routes/api/pages.ts +0 -73
  250. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  251. package/src/routes/dash/index.tsx +0 -109
  252. package/src/routes/dash/media.tsx +0 -135
  253. package/src/routes/dash/pages.tsx +0 -245
  254. package/src/routes/dash/posts.tsx +0 -338
  255. package/src/routes/dash/redirects.tsx +0 -263
  256. package/src/routes/pages/post.tsx +0 -59
  257. package/src/services/__tests__/page.test.ts +0 -298
  258. package/src/services/__tests__/path-registry.test.ts +0 -165
  259. package/src/services/__tests__/redirect.test.ts +0 -159
  260. package/src/services/page.ts +0 -216
  261. package/src/services/path-registry.ts +0 -160
  262. package/src/services/redirect.ts +0 -97
  263. package/src/ui/dash/PageForm.tsx +0 -187
  264. package/src/ui/dash/PostList.tsx +0 -95
  265. package/src/ui/dash/media/MediaListContent.tsx +0 -206
  266. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  267. package/src/ui/dash/pages/PagesContent.tsx +0 -75
  268. package/src/ui/dash/posts/PostForm.tsx +0 -260
  269. package/src/ui/layouts/DashLayout.tsx +0 -247
  270. package/src/ui/pages/SinglePage.tsx +0 -23
  271. package/src/ui/shared/ThreadView.tsx +0 -136
@@ -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
  }
@@ -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
  /**
@@ -11,7 +11,7 @@ describe("Compose Routes", () => {
11
11
  const res = await app.request("/compose", {
12
12
  method: "POST",
13
13
  headers: { "Content-Type": "application/json" },
14
- body: JSON.stringify({ format: "note", body: "Hello" }),
14
+ body: JSON.stringify({ format: "note", bodyMarkdown: "Hello" }),
15
15
  });
16
16
 
17
17
  expect(res.status).toBe(302);
@@ -25,23 +25,22 @@ describe("Compose Routes", () => {
25
25
  const res = await app.request("/compose", {
26
26
  method: "POST",
27
27
  headers: { "Content-Type": "application/json" },
28
- body: JSON.stringify({ format: "note", body: "Hello world" }),
28
+ body: JSON.stringify({ format: "note", bodyMarkdown: "Hello world" }),
29
29
  });
30
30
 
31
31
  expect(res.status).toBe(200);
32
32
  expect(res.headers.get("Content-Type")).toBe("text/event-stream");
33
33
 
34
34
  const text = await res.text();
35
- // SSE prepends the card to the timeline
35
+ // SSE closes the compose dialog and resets signals
36
36
  expect(text).toContain("datastar-patch-elements");
37
- expect(text).toContain('data-format="note"');
38
- expect(text).toContain("selector #timeline-items");
37
+ expect(text).toContain("compose-dialog");
39
38
 
40
39
  // Verify post was created
41
40
  const posts = await services.posts.list();
42
41
  expect(posts).toHaveLength(1);
43
42
  expect(posts[0].format).toBe("note");
44
- expect(posts[0].body).toBe("Hello world");
43
+ expect(posts[0].bodyText).toBe("Hello world");
45
44
  expect(posts[0].status).toBe("published");
46
45
  });
47
46
 
@@ -54,7 +53,7 @@ describe("Compose Routes", () => {
54
53
  headers: { "Content-Type": "application/json" },
55
54
  body: JSON.stringify({
56
55
  format: "link",
57
- body: "Check this out",
56
+ bodyMarkdown: "Check this out",
58
57
  url: "https://example.com",
59
58
  }),
60
59
  });
@@ -62,9 +61,6 @@ describe("Compose Routes", () => {
62
61
  expect(res.status).toBe(200);
63
62
  expect(res.headers.get("Content-Type")).toBe("text/event-stream");
64
63
 
65
- const text = await res.text();
66
- expect(text).toContain('data-format="link"');
67
-
68
64
  const posts = await services.posts.list();
69
65
  expect(posts).toHaveLength(1);
70
66
  expect(posts[0].format).toBe("link");
@@ -80,7 +76,7 @@ describe("Compose Routes", () => {
80
76
  headers: { "Content-Type": "application/json" },
81
77
  body: JSON.stringify({
82
78
  format: "quote",
83
- body: "Great insight",
79
+ bodyMarkdown: "Great insight",
84
80
  quoteText: "The original quote",
85
81
  url: "https://example.com/source",
86
82
  }),
@@ -89,9 +85,6 @@ describe("Compose Routes", () => {
89
85
  expect(res.status).toBe(200);
90
86
  expect(res.headers.get("Content-Type")).toBe("text/event-stream");
91
87
 
92
- const text = await res.text();
93
- expect(text).toContain('data-format="quote"');
94
-
95
88
  const posts = await services.posts.list();
96
89
  expect(posts).toHaveLength(1);
97
90
  expect(posts[0].format).toBe("quote");
@@ -107,7 +100,7 @@ describe("Compose Routes", () => {
107
100
  headers: { "Content-Type": "application/json" },
108
101
  body: JSON.stringify({
109
102
  format: "note",
110
- body: "Draft content",
103
+ bodyMarkdown: "Draft content",
111
104
  status: "draft",
112
105
  }),
113
106
  });
@@ -133,7 +126,7 @@ describe("Compose Routes", () => {
133
126
  const res = await app.request("/compose", {
134
127
  method: "POST",
135
128
  headers: { "Content-Type": "application/json" },
136
- body: JSON.stringify({ format: "invalid", body: "Hello" }),
129
+ body: JSON.stringify({ format: "invalid", bodyMarkdown: "Hello" }),
137
130
  });
138
131
 
139
132
  expect(res.status).toBe(200);
@@ -163,7 +156,7 @@ describe("Compose Routes", () => {
163
156
  headers: { "Content-Type": "application/json" },
164
157
  body: JSON.stringify({
165
158
  format: "note",
166
- body: "Post with media",
159
+ bodyMarkdown: "Post with media",
167
160
  mediaIds: [media.id],
168
161
  }),
169
162
  });
@@ -186,7 +179,7 @@ describe("Compose Routes", () => {
186
179
  const res = await app.request("/compose", {
187
180
  method: "POST",
188
181
  headers: { "Content-Type": "application/json" },
189
- body: JSON.stringify({ format: "note", body: "Hello" }),
182
+ body: JSON.stringify({ format: "note", bodyMarkdown: "Hello" }),
190
183
  });
191
184
 
192
185
  const text = await res.text();
@@ -202,7 +195,7 @@ describe("Compose Routes", () => {
202
195
  const res = await app.request("/compose", {
203
196
  method: "POST",
204
197
  headers: { "Content-Type": "application/json" },
205
- body: JSON.stringify({ body: "No format" }),
198
+ body: JSON.stringify({ bodyMarkdown: "No format" }),
206
199
  });
207
200
 
208
201
  expect(res.status).toBe(200);
@@ -222,7 +215,7 @@ describe("Compose Routes", () => {
222
215
  "Content-Type": "application/json",
223
216
  Accept: "application/json",
224
217
  },
225
- body: JSON.stringify({ format: "note", body: "Hello JSON" }),
218
+ body: JSON.stringify({ format: "note", bodyMarkdown: "Hello JSON" }),
226
219
  });
227
220
 
228
221
  expect(res.status).toBe(200);
@@ -230,11 +223,11 @@ describe("Compose Routes", () => {
230
223
 
231
224
  const data = await res.json();
232
225
  expect(data.status).toBe("published");
233
- expect(data.cardHtml).toContain('data-format="note"');
226
+ expect(data.permalink).toBeDefined();
234
227
 
235
228
  const posts = await services.posts.list();
236
229
  expect(posts).toHaveLength(1);
237
- expect(posts[0].body).toBe("Hello JSON");
230
+ expect(posts[0].bodyText).toBe("Hello JSON");
238
231
  });
239
232
 
240
233
  it("returns JSON for draft", async () => {
@@ -249,7 +242,7 @@ describe("Compose Routes", () => {
249
242
  },
250
243
  body: JSON.stringify({
251
244
  format: "note",
252
- body: "Draft JSON",
245
+ bodyMarkdown: "Draft JSON",
253
246
  status: "draft",
254
247
  }),
255
248
  });
@@ -274,7 +267,7 @@ describe("Compose Routes", () => {
274
267
  "Content-Type": "application/json",
275
268
  Accept: "application/json",
276
269
  },
277
- body: JSON.stringify({ format: "invalid", body: "Hello" }),
270
+ body: JSON.stringify({ format: "invalid", bodyMarkdown: "Hello" }),
278
271
  });
279
272
 
280
273
  expect(res.status).toBe(422);