@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.
- package/bin/commands/export.js +1 -1
- package/bin/commands/import-site.js +529 -0
- package/bin/commands/reset-password.js +3 -2
- package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
- package/dist/client/assets/url-FWFqPJPb.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +4012 -3276
- package/dist/index.js +10285 -5809
- package/package.json +11 -3
- package/src/__tests__/helpers/app.ts +9 -9
- package/src/__tests__/helpers/db.ts +91 -93
- package/src/app.tsx +157 -27
- package/src/auth.ts +20 -2
- package/src/client/archive-nav.js +187 -0
- package/src/client/audio-player.ts +478 -0
- package/src/client/audio-processor.ts +84 -0
- package/src/client/avatar-upload.ts +3 -2
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +645 -49
- package/src/client/components/__tests__/jant-compose-editor.test.ts +208 -16
- package/src/client/components/__tests__/jant-post-form.test.ts +19 -9
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +7 -9
- package/src/client/components/compose-types.ts +101 -4
- package/src/client/components/jant-collection-form.ts +43 -7
- package/src/client/components/jant-collection-sidebar.ts +88 -84
- package/src/client/components/jant-compose-dialog.ts +1655 -219
- package/src/client/components/jant-compose-editor.ts +732 -168
- package/src/client/components/jant-compose-fullscreen.ts +23 -78
- package/src/client/components/jant-media-lightbox.ts +2 -0
- package/src/client/components/jant-nav-manager.ts +24 -284
- package/src/client/components/jant-post-form.ts +89 -9
- package/src/client/components/jant-post-menu.ts +1019 -0
- package/src/client/components/jant-settings-avatar.ts +3 -3
- package/src/client/components/jant-settings-general.ts +38 -4
- package/src/client/components/jant-text-preview.ts +232 -0
- package/src/client/components/nav-manager-types.ts +4 -19
- package/src/client/components/post-form-template.ts +107 -12
- package/src/client/components/post-form-types.ts +11 -4
- package/src/client/compose-bridge.ts +410 -109
- package/src/client/image-processor.ts +26 -8
- package/src/client/media-metadata.ts +247 -0
- package/src/client/multipart-upload.ts +160 -0
- package/src/client/post-form-bridge.ts +52 -1
- package/src/client/settings-bridge.ts +0 -12
- package/src/client/thread-context.ts +140 -0
- package/src/client/tiptap/create-editor.ts +46 -0
- package/src/client/tiptap/extensions.ts +5 -0
- package/src/client/tiptap/image-node.ts +2 -8
- package/src/client/tiptap/paste-image.ts +2 -13
- package/src/client/tiptap/slash-commands.ts +173 -63
- package/src/client/toast.ts +101 -3
- package/src/client/types/sortablejs.d.ts +15 -0
- package/src/client/upload-with-metadata.ts +54 -0
- package/src/client/video-processor.ts +207 -0
- package/src/client.ts +5 -2
- package/src/db/__tests__/migrations.test.ts +118 -0
- package/src/db/index.ts +52 -0
- package/src/db/migrations/0000_baseline.sql +269 -0
- package/src/db/migrations/0001_fts_setup.sql +31 -0
- package/src/db/migrations/meta/0000_snapshot.json +703 -119
- package/src/db/migrations/meta/0001_snapshot.json +1337 -0
- package/src/db/migrations/meta/_journal.json +4 -39
- package/src/db/schema.ts +409 -145
- package/src/i18n/__tests__/detect.test.ts +115 -0
- package/src/i18n/context.tsx +2 -2
- package/src/i18n/detect.ts +85 -1
- package/src/i18n/i18n.ts +1 -1
- package/src/i18n/index.ts +2 -1
- package/src/i18n/locales/en.po +487 -1217
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +613 -996
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +624 -1007
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +6 -0
- package/src/index.ts +5 -7
- package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
- package/src/lib/__tests__/constants.test.ts +0 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
- package/src/lib/__tests__/nanoid.test.ts +26 -0
- package/src/lib/__tests__/schemas.test.ts +181 -63
- package/src/lib/__tests__/slug.test.ts +126 -0
- package/src/lib/__tests__/sse.test.ts +6 -6
- package/src/lib/__tests__/summary.test.ts +264 -0
- package/src/lib/__tests__/theme.test.ts +1 -1
- package/src/lib/__tests__/timeline.test.ts +33 -30
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
- package/src/lib/__tests__/view.test.ts +141 -66
- package/src/lib/blurhash-placeholder.ts +102 -0
- package/src/lib/constants.ts +3 -1
- package/src/lib/emoji-catalog.ts +885 -68
- package/src/lib/errors.ts +11 -8
- package/src/lib/feed.ts +78 -32
- package/src/lib/html.ts +2 -1
- package/src/lib/icon-catalog.ts +5033 -1
- package/src/lib/icons.ts +3 -2
- package/src/lib/index.ts +0 -1
- package/src/lib/markdown-to-tiptap.ts +286 -0
- package/src/lib/media-helpers.ts +12 -3
- package/src/lib/nanoid.ts +29 -0
- package/src/lib/navigation.ts +1 -1
- package/src/lib/render.tsx +20 -2
- package/src/lib/resolve-config.ts +6 -2
- package/src/lib/schemas.ts +224 -55
- package/src/lib/search-snippet.ts +34 -0
- package/src/lib/slug.ts +96 -0
- package/src/lib/sse.ts +6 -6
- package/src/lib/storage.ts +115 -7
- package/src/lib/summary.ts +66 -0
- package/src/lib/theme.ts +11 -8
- package/src/lib/timeline.ts +74 -34
- package/src/lib/tiptap-render.ts +5 -10
- package/src/lib/tiptap-to-markdown.ts +305 -0
- package/src/lib/upload.ts +190 -29
- package/src/lib/url.ts +31 -0
- package/src/lib/view.ts +204 -37
- package/src/middleware/__tests__/auth.test.ts +191 -11
- package/src/middleware/__tests__/onboarding.test.ts +12 -10
- package/src/middleware/auth.ts +63 -9
- package/src/middleware/onboarding.ts +1 -1
- package/src/middleware/secure-headers.ts +40 -0
- package/src/preset.css +45 -2
- package/src/routes/__tests__/compose.test.ts +17 -24
- package/src/routes/api/__tests__/collections.test.ts +109 -61
- package/src/routes/api/__tests__/nav-items.test.ts +46 -29
- package/src/routes/api/__tests__/posts.test.ts +132 -68
- package/src/routes/api/__tests__/search.test.ts +15 -2
- package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
- package/src/routes/api/collections.ts +51 -42
- package/src/routes/api/custom-urls.ts +80 -0
- package/src/routes/api/export.ts +31 -0
- package/src/routes/api/nav-items.ts +23 -19
- package/src/routes/api/posts.ts +43 -39
- package/src/routes/api/search.ts +3 -4
- package/src/routes/api/upload-multipart.ts +245 -0
- package/src/routes/api/upload.ts +85 -19
- package/src/routes/auth/__tests__/setup.test.ts +20 -60
- package/src/routes/auth/setup.tsx +26 -33
- package/src/routes/auth/signin.tsx +3 -7
- package/src/routes/compose.tsx +10 -55
- package/src/routes/dash/__tests__/settings-avatar.test.ts +1 -1
- package/src/routes/dash/custom-urls.tsx +414 -0
- package/src/routes/dash/settings.tsx +304 -232
- package/src/routes/feed/__tests__/rss.test.ts +27 -28
- package/src/routes/feed/rss.ts +6 -4
- package/src/routes/feed/sitemap.ts +2 -12
- package/src/routes/pages/__tests__/collections.test.ts +5 -6
- package/src/routes/pages/__tests__/featured.test.ts +41 -22
- package/src/routes/pages/archive.tsx +175 -39
- package/src/routes/pages/collection.tsx +22 -10
- package/src/routes/pages/collections.tsx +3 -3
- package/src/routes/pages/featured.tsx +28 -4
- package/src/routes/pages/home.tsx +16 -15
- package/src/routes/pages/latest.tsx +1 -11
- package/src/routes/pages/new.tsx +39 -0
- package/src/routes/pages/page.tsx +95 -49
- package/src/routes/pages/search.tsx +1 -1
- package/src/services/__tests__/api-token.test.ts +135 -0
- package/src/services/__tests__/collection.test.ts +275 -227
- package/src/services/__tests__/custom-url.test.ts +213 -0
- package/src/services/__tests__/media.test.ts +162 -22
- package/src/services/__tests__/navigation.test.ts +109 -68
- package/src/services/__tests__/post-timeline.test.ts +205 -32
- package/src/services/__tests__/post.test.ts +713 -234
- package/src/services/__tests__/search.test.ts +67 -10
- package/src/services/api-token.ts +166 -0
- package/src/services/auth.ts +17 -2
- package/src/services/collection.ts +397 -131
- package/src/services/custom-url.ts +188 -0
- package/src/services/export.ts +802 -0
- package/src/services/index.ts +26 -19
- package/src/services/media.ts +100 -22
- package/src/services/navigation.ts +158 -47
- package/src/services/path.ts +339 -0
- package/src/services/post.ts +687 -154
- package/src/services/search.ts +160 -75
- package/src/styles/components.css +58 -7
- package/src/styles/tokens.css +84 -6
- package/src/styles/ui.css +2964 -457
- package/src/types/bindings.ts +4 -1
- package/src/types/config.ts +12 -4
- package/src/types/constants.ts +15 -3
- package/src/types/entities.ts +74 -35
- package/src/types/operations.ts +11 -24
- package/src/types/props.ts +51 -16
- package/src/types/views.ts +45 -22
- package/src/ui/color-themes.ts +133 -23
- package/src/ui/compose/ComposeDialog.tsx +239 -17
- package/src/ui/compose/ComposePrompt.tsx +1 -1
- package/src/ui/dash/CrudPageHeader.tsx +1 -1
- package/src/ui/dash/ListItemRow.tsx +1 -1
- package/src/ui/dash/StatusBadge.tsx +3 -1
- package/src/ui/dash/appearance/AdvancedContent.tsx +22 -1
- package/src/ui/dash/appearance/ColorThemeContent.tsx +22 -2
- package/src/ui/dash/appearance/FontThemeContent.tsx +1 -1
- package/src/ui/dash/appearance/NavigationContent.tsx +5 -45
- package/src/ui/dash/index.ts +0 -3
- package/src/ui/dash/settings/AccountContent.tsx +3 -57
- package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
- package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
- package/src/ui/dash/settings/AvatarContent.tsx +8 -0
- package/src/ui/dash/settings/SessionsContent.tsx +159 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +45 -15
- package/src/ui/feed/LinkCard.tsx +89 -40
- package/src/ui/feed/NoteCard.tsx +39 -25
- package/src/ui/feed/PostStatusBadges.tsx +67 -0
- package/src/ui/feed/QuoteCard.tsx +38 -23
- package/src/ui/feed/ThreadPreview.tsx +90 -26
- package/src/ui/feed/TimelineFeed.tsx +3 -2
- package/src/ui/feed/TimelineItem.tsx +15 -6
- package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
- package/src/ui/feed/thread-preview-state.ts +61 -0
- package/src/ui/font-themes.ts +2 -2
- package/src/ui/layouts/BaseLayout.tsx +2 -2
- package/src/ui/layouts/SiteLayout.tsx +105 -92
- package/src/ui/pages/ArchivePage.tsx +923 -98
- package/src/ui/pages/ComposePage.tsx +54 -0
- package/src/ui/pages/PostPage.tsx +30 -45
- package/src/ui/pages/SearchPage.tsx +181 -37
- package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
- package/src/ui/shared/CollectionsSidebar.tsx +47 -37
- package/src/ui/shared/MediaGallery.tsx +445 -149
- package/src/ui/shared/PostFooter.tsx +204 -0
- package/src/ui/shared/StarRating.tsx +27 -0
- package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
- package/src/ui/shared/index.ts +0 -1
- package/dist/client/assets/url-8Dj-5CLW.js +0 -1
- package/src/client/media-upload.ts +0 -161
- package/src/client/page-slug-bridge.ts +0 -42
- package/src/db/migrations/0000_square_wallflower.sql +0 -118
- package/src/db/migrations/0001_add_search_fts.sql +0 -34
- package/src/db/migrations/0002_add_media_attachments.sql +0 -3
- package/src/db/migrations/0003_add_navigation_links.sql +0 -8
- package/src/db/migrations/0004_add_storage_provider.sql +0 -3
- package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
- package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
- package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
- package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
- package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
- package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
- package/src/db/migrations/0011_add_path_registry.sql +0 -23
- package/src/db/migrations/0012_add_tiptap_columns.sql +0 -2
- package/src/db/migrations/0013_replace_featured_with_visibility.sql +0 -8
- package/src/db/migrations/meta/0003_snapshot.json +0 -821
- package/src/lib/__tests__/sqid.test.ts +0 -65
- package/src/lib/sqid.ts +0 -79
- package/src/routes/api/__tests__/pages.test.ts +0 -218
- package/src/routes/api/pages.ts +0 -73
- package/src/routes/dash/__tests__/pages.test.ts +0 -226
- package/src/routes/dash/index.tsx +0 -109
- package/src/routes/dash/media.tsx +0 -135
- package/src/routes/dash/pages.tsx +0 -245
- package/src/routes/dash/posts.tsx +0 -338
- package/src/routes/dash/redirects.tsx +0 -263
- package/src/routes/pages/post.tsx +0 -59
- package/src/services/__tests__/page.test.ts +0 -298
- package/src/services/__tests__/path-registry.test.ts +0 -165
- package/src/services/__tests__/redirect.test.ts +0 -159
- package/src/services/page.ts +0 -216
- package/src/services/path-registry.ts +0 -160
- package/src/services/redirect.ts +0 -97
- package/src/ui/dash/PageForm.tsx +0 -187
- package/src/ui/dash/PostList.tsx +0 -95
- package/src/ui/dash/media/MediaListContent.tsx +0 -206
- package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
- package/src/ui/dash/pages/PagesContent.tsx +0 -75
- package/src/ui/dash/posts/PostForm.tsx +0 -260
- package/src/ui/layouts/DashLayout.tsx +0 -247
- package/src/ui/pages/SinglePage.tsx +0 -23
- 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("/
|
|
35
|
-
app.get("/
|
|
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.
|
|
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 /
|
|
66
|
+
it("redirects /settings to /setup when onboarding not complete", async () => {
|
|
67
67
|
const { app } = createApp(false);
|
|
68
|
-
const res = await app.request("/
|
|
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 /
|
|
73
|
+
it("redirects /settings/* to /setup when onboarding not complete", async () => {
|
|
74
74
|
const { app } = createApp(false);
|
|
75
|
-
const res = await app.request("/
|
|
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("/
|
|
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("/
|
|
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
|
});
|
package/src/middleware/auth.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
*
|
|
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 (
|
|
48
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
}
|
|
@@ -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
|
-
|
|
28
|
-
|
|
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",
|
|
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",
|
|
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
|
|
35
|
+
// SSE closes the compose dialog and resets signals
|
|
36
36
|
expect(text).toContain("datastar-patch-elements");
|
|
37
|
-
expect(text).toContain(
|
|
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].
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
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",
|
|
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({
|
|
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",
|
|
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.
|
|
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].
|
|
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
|
-
|
|
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",
|
|
270
|
+
body: JSON.stringify({ format: "invalid", bodyMarkdown: "Hello" }),
|
|
278
271
|
});
|
|
279
272
|
|
|
280
273
|
expect(res.status).toBe(422);
|