@jant/core 0.3.4 → 0.3.5

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/dist/app.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EAAc,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAGlD,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAuCvD,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,UAAU,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,GAAG,GAAG,IAAI,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,YAAY,CAAA;CAAE,CAAC,CAAC;AAExE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,SAAS,CAAC,MAAM,GAAE,UAAe,GAAG,GAAG,CAuoBtD"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EAAc,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAGlD,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAwCvD,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,UAAU,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,GAAG,GAAG,IAAI,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,YAAY,CAAA;CAAE,CAAC,CAAC;AAExE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,SAAS,CAAC,MAAM,GAAE,UAAe,GAAG,GAAG,CA0oBtD"}
package/dist/app.js CHANGED
@@ -33,6 +33,7 @@ import { rssRoutes } from "./routes/feed/rss.js";
33
33
  import { sitemapRoutes } from "./routes/feed/sitemap.js";
34
34
  // Middleware
35
35
  import { requireAuth } from "./middleware/auth.js";
36
+ import { requireOnboarding } from "./middleware/onboarding.js";
36
37
  // Layouts for auth pages
37
38
  import { BaseLayout } from "./theme/layouts/index.js";
38
39
  import { dsRedirect, dsToast } from "./lib/sse.js";
@@ -78,6 +79,8 @@ import { getAvailableThemes, buildThemeStyle } from "./lib/theme.js";
78
79
  }
79
80
  await next();
80
81
  });
82
+ // Onboarding gate — redirect to /setup if not yet initialized
83
+ app.use("*", requireOnboarding());
81
84
  // Theme middleware - resolve active color theme and build CSS
82
85
  app.use("*", async (c, next)=>{
83
86
  const themeId = await c.var.services.settings.get(SETTINGS_KEYS.THEME);
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Onboarding Middleware
3
+ *
4
+ * Redirects all requests to /setup if onboarding hasn't been completed.
5
+ * Caches the result in memory so the DB is only queried once per isolate lifetime.
6
+ */
7
+ import type { MiddlewareHandler } from "hono";
8
+ import type { Bindings } from "../types.js";
9
+ import type { AppVariables } from "../app.js";
10
+ type Env = {
11
+ Bindings: Bindings;
12
+ Variables: AppVariables;
13
+ };
14
+ /**
15
+ * Middleware that redirects to /setup if onboarding is not complete.
16
+ * Uses module-level caching: once onboarding is confirmed complete,
17
+ * no further DB queries are made for the lifetime of the Worker isolate.
18
+ */
19
+ export declare function requireOnboarding(): MiddlewareHandler<Env>;
20
+ /**
21
+ * Reset the onboarding cache. Only for testing.
22
+ * @internal
23
+ */
24
+ export declare function resetOnboardingCache(): void;
25
+ export {};
26
+ //# sourceMappingURL=onboarding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"onboarding.d.ts","sourceRoot":"","sources":["../../src/middleware/onboarding.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE9C,KAAK,GAAG,GAAG;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,YAAY,CAAA;CAAE,CAAC;AAK3D;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,iBAAiB,CAAC,GAAG,CAAC,CAmB1D;AAaD;;;GAGG;AACH,wBAAgB,oBAAoB,SAEnC"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Onboarding Middleware
3
+ *
4
+ * Redirects all requests to /setup if onboarding hasn't been completed.
5
+ * Caches the result in memory so the DB is only queried once per isolate lifetime.
6
+ */ /** In-memory cache — persists across requests within a Worker isolate */ let onboardingComplete = false;
7
+ /**
8
+ * Middleware that redirects to /setup if onboarding is not complete.
9
+ * Uses module-level caching: once onboarding is confirmed complete,
10
+ * no further DB queries are made for the lifetime of the Worker isolate.
11
+ */ export function requireOnboarding() {
12
+ return async (c, next)=>{
13
+ if (onboardingComplete) {
14
+ return next();
15
+ }
16
+ const path = new URL(c.req.url).pathname;
17
+ if (shouldBypass(path)) {
18
+ return next();
19
+ }
20
+ const isComplete = await c.var.services.settings.isOnboardingComplete();
21
+ if (isComplete) {
22
+ onboardingComplete = true;
23
+ return next();
24
+ }
25
+ return c.redirect("/setup");
26
+ };
27
+ }
28
+ function shouldBypass(path) {
29
+ return path === "/setup" || path === "/health" || path === "/signin" || path === "/signout" || path === "/reset" || path.startsWith("/api/auth/");
30
+ }
31
+ /**
32
+ * Reset the onboarding cache. Only for testing.
33
+ * @internal
34
+ */ export function resetOnboardingCache() {
35
+ onboardingComplete = false;
36
+ }
@@ -100,10 +100,6 @@ function HomeContent({ siteName, posts }) {
100
100
  });
101
101
  }
102
102
  homeRoutes.get("/", async (c)=>{
103
- const isComplete = await c.var.services.settings.isOnboardingComplete();
104
- if (!isComplete) {
105
- return c.redirect("/setup");
106
- }
107
103
  const siteName = await getSiteName(c);
108
104
  const posts = await c.var.services.posts.list({
109
105
  visibility: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jant/core",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "A modern, open-source microblogging platform built on Cloudflare Workers",
5
5
  "type": "module",
6
6
  "bin": {
package/src/app.tsx CHANGED
@@ -41,6 +41,7 @@ import { sitemapRoutes } from "./routes/feed/sitemap.js";
41
41
 
42
42
  // Middleware
43
43
  import { requireAuth } from "./middleware/auth.js";
44
+ import { requireOnboarding } from "./middleware/onboarding.js";
44
45
 
45
46
  // Layouts for auth pages
46
47
  import { BaseLayout } from "./theme/layouts/index.js";
@@ -104,6 +105,9 @@ export function createApp(config: JantConfig = {}): App {
104
105
  await next();
105
106
  });
106
107
 
108
+ // Onboarding gate — redirect to /setup if not yet initialized
109
+ app.use("*", requireOnboarding());
110
+
107
111
  // Theme middleware - resolve active color theme and build CSS
108
112
  app.use("*", async (c, next) => {
109
113
  const themeId = await c.var.services.settings.get(SETTINGS_KEYS.THEME);
@@ -0,0 +1,143 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { Hono } from "hono";
3
+ import { requireOnboarding, resetOnboardingCache } from "../onboarding.js";
4
+ import type { Bindings } from "../../types.js";
5
+ import type { AppVariables } from "../../app.js";
6
+
7
+ type Env = { Bindings: Bindings; Variables: AppVariables };
8
+
9
+ function createMockServices(complete: boolean) {
10
+ let callCount = 0;
11
+ const services = {
12
+ settings: {
13
+ isOnboardingComplete: async () => {
14
+ callCount++;
15
+ return complete;
16
+ },
17
+ },
18
+ } as AppVariables["services"];
19
+ return { services, getCallCount: () => callCount };
20
+ }
21
+
22
+ function createApp(complete: boolean) {
23
+ const mock = createMockServices(complete);
24
+ const app = new Hono<Env>();
25
+
26
+ app.use("*", async (c, next) => {
27
+ c.set("services", mock.services);
28
+ await next();
29
+ });
30
+ app.use("*", requireOnboarding());
31
+
32
+ // Register routes for testing
33
+ app.get("/", (c) => c.text("Home"));
34
+ app.get("/dash", (c) => c.text("Dashboard"));
35
+ app.get("/archive", (c) => c.text("Archive"));
36
+ app.get("/p/abc", (c) => c.text("Post"));
37
+ app.get("/setup", (c) => c.text("Setup"));
38
+ app.get("/health", (c) => c.text("OK"));
39
+ app.get("/signin", (c) => c.text("Signin"));
40
+ app.get("/signout", (c) => c.text("Signout"));
41
+ app.get("/reset", (c) => c.text("Reset"));
42
+ app.get("/api/auth/session", (c) => c.json({ ok: true }));
43
+
44
+ return { app, getCallCount: mock.getCallCount };
45
+ }
46
+
47
+ describe("requireOnboarding", () => {
48
+ beforeEach(() => {
49
+ resetOnboardingCache();
50
+ });
51
+
52
+ it("redirects to /setup when onboarding not complete", async () => {
53
+ const { app } = createApp(false);
54
+ const res = await app.request("/", { redirect: "manual" });
55
+ expect(res.status).toBe(302);
56
+ expect(res.headers.get("Location")).toBe("/setup");
57
+ });
58
+
59
+ it("redirects /dash to /setup when onboarding not complete", async () => {
60
+ const { app } = createApp(false);
61
+ const res = await app.request("/dash", { redirect: "manual" });
62
+ expect(res.status).toBe(302);
63
+ expect(res.headers.get("Location")).toBe("/setup");
64
+ });
65
+
66
+ it("redirects /archive to /setup when onboarding not complete", async () => {
67
+ const { app } = createApp(false);
68
+ const res = await app.request("/archive", { redirect: "manual" });
69
+ expect(res.status).toBe(302);
70
+ expect(res.headers.get("Location")).toBe("/setup");
71
+ });
72
+
73
+ it("allows through when onboarding is complete", async () => {
74
+ const { app } = createApp(true);
75
+ const res = await app.request("/");
76
+ expect(res.status).toBe(200);
77
+ expect(await res.text()).toBe("Home");
78
+ });
79
+
80
+ it("caches result — second request skips DB query", async () => {
81
+ const { app, getCallCount } = createApp(true);
82
+
83
+ await app.request("/");
84
+ expect(getCallCount()).toBe(1);
85
+
86
+ await app.request("/dash");
87
+ expect(getCallCount()).toBe(1); // still 1 — cached
88
+ });
89
+
90
+ it("does not cache incomplete status", async () => {
91
+ const { app, getCallCount } = createApp(false);
92
+
93
+ await app.request("/", { redirect: "manual" });
94
+ expect(getCallCount()).toBe(1);
95
+
96
+ await app.request("/dash", { redirect: "manual" });
97
+ expect(getCallCount()).toBe(2); // queried again
98
+ });
99
+
100
+ describe("bypass paths", () => {
101
+ it("allows /setup without checking DB", async () => {
102
+ const { app, getCallCount } = createApp(false);
103
+ const res = await app.request("/setup");
104
+ expect(res.status).toBe(200);
105
+ expect(getCallCount()).toBe(0);
106
+ });
107
+
108
+ it("allows /health without checking DB", async () => {
109
+ const { app, getCallCount } = createApp(false);
110
+ const res = await app.request("/health");
111
+ expect(res.status).toBe(200);
112
+ expect(getCallCount()).toBe(0);
113
+ });
114
+
115
+ it("allows /signin without checking DB", async () => {
116
+ const { app, getCallCount } = createApp(false);
117
+ const res = await app.request("/signin");
118
+ expect(res.status).toBe(200);
119
+ expect(getCallCount()).toBe(0);
120
+ });
121
+
122
+ it("allows /signout without checking DB", async () => {
123
+ const { app, getCallCount } = createApp(false);
124
+ const res = await app.request("/signout");
125
+ expect(res.status).toBe(200);
126
+ expect(getCallCount()).toBe(0);
127
+ });
128
+
129
+ it("allows /reset without checking DB", async () => {
130
+ const { app, getCallCount } = createApp(false);
131
+ const res = await app.request("/reset");
132
+ expect(res.status).toBe(200);
133
+ expect(getCallCount()).toBe(0);
134
+ });
135
+
136
+ it("allows /api/auth/* without checking DB", async () => {
137
+ const { app, getCallCount } = createApp(false);
138
+ const res = await app.request("/api/auth/session");
139
+ expect(res.status).toBe(200);
140
+ expect(getCallCount()).toBe(0);
141
+ });
142
+ });
143
+ });
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Onboarding Middleware
3
+ *
4
+ * Redirects all requests to /setup if onboarding hasn't been completed.
5
+ * Caches the result in memory so the DB is only queried once per isolate lifetime.
6
+ */
7
+
8
+ import type { MiddlewareHandler } from "hono";
9
+ import type { Bindings } from "../types.js";
10
+ import type { AppVariables } from "../app.js";
11
+
12
+ type Env = { Bindings: Bindings; Variables: AppVariables };
13
+
14
+ /** In-memory cache — persists across requests within a Worker isolate */
15
+ let onboardingComplete = false;
16
+
17
+ /**
18
+ * Middleware that redirects to /setup if onboarding is not complete.
19
+ * Uses module-level caching: once onboarding is confirmed complete,
20
+ * no further DB queries are made for the lifetime of the Worker isolate.
21
+ */
22
+ export function requireOnboarding(): MiddlewareHandler<Env> {
23
+ return async (c, next) => {
24
+ if (onboardingComplete) {
25
+ return next();
26
+ }
27
+
28
+ const path = new URL(c.req.url).pathname;
29
+ if (shouldBypass(path)) {
30
+ return next();
31
+ }
32
+
33
+ const isComplete = await c.var.services.settings.isOnboardingComplete();
34
+ if (isComplete) {
35
+ onboardingComplete = true;
36
+ return next();
37
+ }
38
+
39
+ return c.redirect("/setup");
40
+ };
41
+ }
42
+
43
+ function shouldBypass(path: string): boolean {
44
+ return (
45
+ path === "/setup" ||
46
+ path === "/health" ||
47
+ path === "/signin" ||
48
+ path === "/signout" ||
49
+ path === "/reset" ||
50
+ path.startsWith("/api/auth/")
51
+ );
52
+ }
53
+
54
+ /**
55
+ * Reset the onboarding cache. Only for testing.
56
+ * @internal
57
+ */
58
+ export function resetOnboardingCache() {
59
+ onboardingComplete = false;
60
+ }
@@ -102,11 +102,6 @@ function HomeContent({ siteName, posts }: { siteName: string; posts: Post[] }) {
102
102
  }
103
103
 
104
104
  homeRoutes.get("/", async (c) => {
105
- const isComplete = await c.var.services.settings.isOnboardingComplete();
106
- if (!isComplete) {
107
- return c.redirect("/setup");
108
- }
109
-
110
105
  const siteName = await getSiteName(c);
111
106
 
112
107
  const posts = await c.var.services.posts.list({