@jant/core 0.3.36 → 0.3.38

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
@@ -1,80 +1,126 @@
1
1
  /**
2
- * Custom Page Route
2
+ * Catch-all Route
3
3
  *
4
- * Serves pages and posts with custom paths via the path registry.
5
- * This is a catch-all route mounted at "/" - must be registered last.
6
- * The path registry eliminates ambiguity: each path maps to exactly
7
- * one entity (page, post, or redirect).
4
+ * Resolves post slugs, aliases, redirects, and collection aliases.
5
+ * Must be registered last.
8
6
  */
9
7
 
10
- import { Hono } from "hono";
8
+ import { Hono, type Context } from "hono";
11
9
  import type { Bindings } from "../../types.js";
12
10
  import type { AppVariables } from "../../types/app-context.js";
13
- import { SinglePage } from "../../ui/pages/SinglePage.js";
14
11
  import { PostPage } from "../../ui/pages/PostPage.js";
15
12
  import { getNavigationData } from "../../lib/navigation.js";
16
13
  import { renderPublicPage } from "../../lib/render.js";
17
14
  import { buildMediaMap } from "../../lib/media-helpers.js";
18
- import { createMediaContext, toPageView, toPostView } from "../../lib/view.js";
15
+ import { createMediaContext, toPostView } from "../../lib/view.js";
16
+ import type { Post } from "../../types.js";
19
17
 
20
18
  type Env = { Bindings: Bindings; Variables: AppVariables };
21
19
 
22
20
  export const pageRoutes = new Hono<Env>();
23
21
 
24
- // Catch-all for custom page slugs and post paths (including multi-level)
22
+ async function renderPost(c: Context<Env>, post: Post) {
23
+ const mediaCtx = createMediaContext(c.var.appConfig);
24
+
25
+ // Load the full thread if this post is part of one
26
+ const threadRootId = post.threadId;
27
+ const threadPosts = (
28
+ await c.var.services.posts.getThread(threadRootId)
29
+ ).filter((threadPost) => threadPost.status === "published");
30
+
31
+ // Batch load media for all thread posts (or just this post if solo)
32
+ const allPostIds =
33
+ threadPosts.length > 1 ? threadPosts.map((p) => p.id) : [post.id];
34
+ const rawMediaMap = await c.var.services.media.getByPostIds(allPostIds);
35
+ const mediaMap = buildMediaMap(
36
+ rawMediaMap,
37
+ mediaCtx.r2PublicUrl,
38
+ mediaCtx.imageTransformUrl,
39
+ mediaCtx.s3PublicUrl,
40
+ );
41
+
42
+ // Batch load collections for all posts
43
+ const collectionsMap =
44
+ await c.var.services.collections.getCollectionsByPostIds(allPostIds);
45
+
46
+ const postView = toPostView(
47
+ { ...post, mediaAttachments: mediaMap.get(post.id) ?? [] },
48
+ mediaCtx,
49
+ collectionsMap.get(post.id),
50
+ );
51
+
52
+ // Build thread post views if this is a multi-post thread
53
+ const threadPostViews =
54
+ threadPosts.length > 1
55
+ ? threadPosts.map((tp, i) =>
56
+ toPostView(
57
+ { ...tp, mediaAttachments: mediaMap.get(tp.id) ?? [] },
58
+ mediaCtx,
59
+ collectionsMap.get(tp.id),
60
+ undefined,
61
+ i === threadPosts.length - 1,
62
+ ),
63
+ )
64
+ : undefined;
65
+
66
+ const navData = await getNavigationData(c);
67
+ const title = post.title || navData.siteName;
68
+
69
+ return renderPublicPage(c, {
70
+ title,
71
+ description: post.body?.slice(0, 160),
72
+ navData,
73
+ content: <PostPage post={postView} threadPosts={threadPostViews} />,
74
+ });
75
+ }
76
+
77
+ // Catch-all for path-registry backed post URLs, aliases, and redirects
25
78
  pageRoutes.get("/*", async (c) => {
26
79
  const fullPath = c.req.path.slice(1); // Remove leading /
27
80
  if (!fullPath) return c.notFound();
28
81
 
29
- const entry = await c.var.services.pathRegistry.getByPath(fullPath);
82
+ const resolved = await c.var.services.paths.resolve(fullPath);
83
+ if (!resolved) return c.notFound();
30
84
 
31
- if (entry?.ownerType === "page") {
32
- const page = await c.var.services.pages.getById(entry.ownerId);
33
- if (!page || page.status === "draft") {
34
- return c.notFound();
35
- }
85
+ if (resolved.kind === "redirect" && resolved.redirectToPath) {
86
+ return c.redirect(
87
+ `/${resolved.redirectToPath}`,
88
+ resolved.redirectType ?? 301,
89
+ );
90
+ }
36
91
 
37
- const navData = await getNavigationData(c);
38
- const pageView = toPageView(page);
92
+ if (resolved.postId) {
93
+ const post = await c.var.services.posts.getById(resolved.postId);
94
+ if (!post || post.status === "draft") return c.notFound();
39
95
 
40
- return renderPublicPage(c, {
41
- title: `${page.title || fullPath} - ${navData.siteName}`,
42
- description: page.body?.slice(0, 160),
43
- navData,
44
- content: <SinglePage page={pageView} />,
45
- });
46
- }
96
+ if (post.visibility === "private") {
97
+ const navData = await getNavigationData(c);
98
+ if (!navData.isAuthenticated) return c.notFound();
99
+ }
47
100
 
48
- if (entry?.ownerType === "post") {
49
- const post = await c.var.services.posts.getById(entry.ownerId);
50
- if (!post || post.status === "draft") {
51
- return c.notFound();
101
+ // If accessed via slug but an alias exists, redirect to the alias
102
+ if (resolved.kind === "slug") {
103
+ const alias = await c.var.services.customUrls.getByTarget(
104
+ "post",
105
+ post.id,
106
+ );
107
+ if (alias) {
108
+ return c.redirect(`/${alias.path}`, 301);
109
+ }
52
110
  }
53
111
 
54
- // Load media attachments
55
- const rawMediaMap = await c.var.services.media.getByPostIds([post.id]);
56
- const mediaCtx = createMediaContext(c.var.appConfig);
57
- const mediaMap = buildMediaMap(
58
- rawMediaMap,
59
- mediaCtx.r2PublicUrl,
60
- mediaCtx.imageTransformUrl,
61
- mediaCtx.s3PublicUrl,
62
- );
112
+ return renderPost(c, post);
113
+ }
63
114
 
64
- const postView = toPostView(
65
- { ...post, mediaAttachments: mediaMap.get(post.id) ?? [] },
66
- mediaCtx,
115
+ if (resolved.collectionId) {
116
+ const collection = await c.var.services.collections.getById(
117
+ resolved.collectionId,
67
118
  );
119
+ if (!collection) return c.notFound();
68
120
 
69
- const navData = await getNavigationData(c);
70
- const title = post.title || navData.siteName;
71
-
72
- return renderPublicPage(c, {
73
- title,
74
- description: post.body?.slice(0, 160),
75
- navData,
76
- content: <PostPage post={postView} />,
77
- });
121
+ if (resolved.kind === "alias") {
122
+ return c.redirect(`/c/${collection.slug}`, 301);
123
+ }
78
124
  }
79
125
 
80
126
  return c.notFound();
@@ -50,7 +50,7 @@ searchRoutes.get("/", async (c) => {
50
50
 
51
51
  // Transform to View Models
52
52
  const mediaCtx = createMediaContext(c.var.appConfig);
53
- const resultViews = toSearchResultViews(results, mediaCtx);
53
+ const resultViews = toSearchResultViews(results, mediaCtx, query);
54
54
 
55
55
  return renderPublicPage(c, {
56
56
  title: query
@@ -0,0 +1,135 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { createTestDatabase } from "../../__tests__/helpers/db.js";
3
+ import { createApiTokenService } from "../api-token.js";
4
+ import type { Database } from "../../db/index.js";
5
+
6
+ describe("ApiTokenService", () => {
7
+ let db: Database;
8
+ let service: ReturnType<typeof createApiTokenService>;
9
+
10
+ beforeEach(() => {
11
+ const testDb = createTestDatabase();
12
+ db = testDb.db as unknown as Database;
13
+ service = createApiTokenService(db);
14
+ });
15
+
16
+ describe("create", () => {
17
+ it("returns a token with jnt_ prefix and metadata", async () => {
18
+ const { token, plaintext } = await service.create("Test Token");
19
+
20
+ expect(plaintext).toMatch(/^jnt_[0-9a-f]{64}$/);
21
+ expect(token.name).toBe("Test Token");
22
+ expect(token.prefix).toHaveLength(8);
23
+ expect(token.lastUsedAt).toBeNull();
24
+ expect(token.createdAt).toBeGreaterThan(0);
25
+ expect(token.updatedAt).toBeGreaterThan(0);
26
+ expect(token.id).toBeTruthy();
27
+ });
28
+
29
+ it("does not expose tokenHash in the returned entity", async () => {
30
+ const { token } = await service.create("Test");
31
+
32
+ expect(token).not.toHaveProperty("tokenHash");
33
+ });
34
+
35
+ it("generates unique tokens each time", async () => {
36
+ const { plaintext: t1 } = await service.create("Token 1");
37
+ const { plaintext: t2 } = await service.create("Token 2");
38
+
39
+ expect(t1).not.toBe(t2);
40
+ });
41
+ });
42
+
43
+ describe("list", () => {
44
+ it("returns empty array when no tokens exist", async () => {
45
+ const tokens = await service.list();
46
+ expect(tokens).toEqual([]);
47
+ });
48
+
49
+ it("returns all created tokens", async () => {
50
+ await service.create("Token A");
51
+ await service.create("Token B");
52
+
53
+ const tokens = await service.list();
54
+ expect(tokens).toHaveLength(2);
55
+ expect(tokens[0]?.name).toBe("Token A");
56
+ expect(tokens[1]?.name).toBe("Token B");
57
+ });
58
+
59
+ it("does not include tokenHash in listed tokens", async () => {
60
+ await service.create("Token");
61
+
62
+ const tokens = await service.list();
63
+ expect(tokens[0]).not.toHaveProperty("tokenHash");
64
+ });
65
+ });
66
+
67
+ describe("verify", () => {
68
+ it("returns token ID for valid token", async () => {
69
+ const { token, plaintext } = await service.create("Test");
70
+
71
+ const result = await service.verify(plaintext);
72
+ expect(result).toBe(token.id);
73
+ });
74
+
75
+ it("returns null for invalid token", async () => {
76
+ await service.create("Test");
77
+
78
+ const result = await service.verify("jnt_" + "0".repeat(64));
79
+ expect(result).toBeNull();
80
+ });
81
+
82
+ it("returns null for token without jnt_ prefix", async () => {
83
+ const result = await service.verify("invalid_token");
84
+ expect(result).toBeNull();
85
+ });
86
+
87
+ it("returns null for empty string", async () => {
88
+ const result = await service.verify("");
89
+ expect(result).toBeNull();
90
+ });
91
+ });
92
+
93
+ describe("delete", () => {
94
+ it("returns true when token exists", async () => {
95
+ const { token } = await service.create("Test");
96
+
97
+ const result = await service.delete(token.id);
98
+ expect(result).toBe(true);
99
+ });
100
+
101
+ it("returns false when token does not exist", async () => {
102
+ const result = await service.delete("nonexistent-id");
103
+ expect(result).toBe(false);
104
+ });
105
+
106
+ it("prevents verification of deleted token", async () => {
107
+ const { token, plaintext } = await service.create("Test");
108
+ await service.delete(token.id);
109
+
110
+ const result = await service.verify(plaintext);
111
+ expect(result).toBeNull();
112
+ });
113
+
114
+ it("removes token from list", async () => {
115
+ const { token } = await service.create("Test");
116
+ await service.delete(token.id);
117
+
118
+ const tokens = await service.list();
119
+ expect(tokens).toHaveLength(0);
120
+ });
121
+ });
122
+
123
+ describe("updateLastUsed", () => {
124
+ it("updates the lastUsedAt timestamp", async () => {
125
+ const { token } = await service.create("Test");
126
+ expect(token.lastUsedAt).toBeNull();
127
+
128
+ await service.updateLastUsed(token.id);
129
+
130
+ const tokens = await service.list();
131
+ const updated = tokens.find((t) => t.id === token.id);
132
+ expect(updated?.lastUsedAt).toBeGreaterThan(0);
133
+ });
134
+ });
135
+ });