create-restack-pattern 0.1.8 → 0.2.1

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 (119) hide show
  1. package/dist/index.js +284 -74
  2. package/package.json +14 -8
  3. package/template/{DEVELOPMENT.md → base/DEVELOPMENT.md} +73 -62
  4. package/template/{backend → base/backend}/package.json +13 -10
  5. package/template/{backend → base/backend}/src/config/env.ts +2 -2
  6. package/template/{backend → base/backend}/src/domains/auth/auth.model.ts +5 -3
  7. package/template/{backend → base/backend}/src/domains/auth/auth.repo.ts +23 -5
  8. package/template/{backend → base/backend}/src/domains/auth/auth.routes.ts +77 -22
  9. package/template/{backend → base/backend}/src/domains/auth/auth.service.test.ts +51 -19
  10. package/template/{backend → base/backend}/src/domains/auth/auth.service.ts +15 -10
  11. package/template/{backend → base/backend}/src/domains/auth/auth.types.ts +1 -1
  12. package/template/{backend → base/backend}/src/domains/user/user.model.ts +2 -1
  13. package/template/{backend → base/backend}/src/domains/user/user.repo.ts +3 -2
  14. package/template/base/backend/src/domains/user/user.routes.test.ts +127 -0
  15. package/template/{backend → base/backend}/src/domains/user/user.routes.ts +19 -9
  16. package/template/{backend → base/backend}/src/domains/user/user.service.ts +8 -2
  17. package/template/{backend → base/backend}/src/hono-app.ts +6 -11
  18. package/template/base/backend/src/infra/middlewares/require-auth.test.ts +91 -0
  19. package/template/base/backend/src/infra/middlewares/require-auth.ts +63 -0
  20. package/template/base/backend/src/infra/middlewares/require-role.test.ts +66 -0
  21. package/template/base/backend/src/infra/middlewares/require-role.ts +34 -0
  22. package/template/{backend → base/backend}/src/utils/logger.ts +7 -10
  23. package/template/base/backend/src/utils/uuid.test.ts +37 -0
  24. package/template/{backend → base/backend}/tsconfig.json +4 -0
  25. package/template/base/frontend/AGENTS.md +11 -0
  26. package/template/{frontend → base/frontend}/app/(auth)/login/page.tsx +1 -1
  27. package/template/{frontend → base/frontend}/app/(auth)/register/page.tsx +1 -1
  28. package/template/{frontend → base/frontend}/app/globals.css +1 -1
  29. package/template/{frontend → base/frontend}/app/layout.tsx +9 -12
  30. package/template/{frontend → base/frontend}/app/page.tsx +2 -2
  31. package/template/{frontend → base/frontend}/app/profile/page.tsx +3 -3
  32. package/template/{frontend → base/frontend}/components/ui/button.tsx +2 -2
  33. package/template/{frontend → base/frontend}/components/ui/card.tsx +8 -23
  34. package/template/{frontend → base/frontend}/components/ui/input.tsx +2 -2
  35. package/template/{frontend → base/frontend}/components/ui/label.tsx +3 -6
  36. package/template/{frontend → base/frontend}/domains/auth/components/login-form.tsx +10 -3
  37. package/template/{frontend → base/frontend}/domains/auth/components/logout-button.tsx +2 -2
  38. package/template/{frontend → base/frontend}/domains/auth/components/register-form.tsx +10 -3
  39. package/template/{frontend → base/frontend}/domains/auth/hooks/use-login.ts +2 -2
  40. package/template/{frontend → base/frontend}/domains/auth/hooks/use-register.ts +6 -3
  41. package/template/{frontend → base/frontend}/domains/auth/services/auth.api.ts +5 -7
  42. package/template/{frontend → base/frontend}/domains/user/components/profile-card.tsx +2 -4
  43. package/template/{frontend → base/frontend}/domains/user/services/user.api.ts +2 -3
  44. package/template/base/frontend/env.ts +12 -0
  45. package/template/{frontend → base/frontend}/hooks/use-auth-mutation.ts +6 -3
  46. package/template/{frontend → base/frontend}/lib/api-client.ts +40 -5
  47. package/template/{frontend → base/frontend}/lib/utils.ts +1 -1
  48. package/template/base/frontend/next.config.ts +11 -0
  49. package/template/{frontend → base/frontend}/package.json +15 -14
  50. package/template/{frontend → base/frontend}/tsconfig.json +4 -0
  51. package/template/{gitignore → base/gitignore} +15 -0
  52. package/template/base/package.json +25 -0
  53. package/template/{packages → base/packages}/shared/package.json +1 -1
  54. package/template/{packages → base/packages}/shared/src/index.ts +1 -0
  55. package/template/{packages → base/packages}/shared/src/schemas/auth.schema.ts +8 -0
  56. package/template/{packages → base/packages}/shared/src/schemas/user.schema.ts +1 -0
  57. package/template/base/packages/shared/src/utils/uuid.ts +40 -0
  58. package/template/{packages → base/packages}/shared/tsconfig.json +26 -0
  59. package/template/base/pnpm-workspace.yaml +12 -0
  60. package/template/overlays/db-postgres/backend/.env.example +6 -0
  61. package/template/overlays/db-postgres/backend/drizzle.config.ts +11 -0
  62. package/template/overlays/db-postgres/backend/scripts/db-check.js +27 -0
  63. package/template/overlays/db-postgres/backend/src/infra/db/index.ts +6 -0
  64. package/template/overlays/db-sqlite/backend/.env.example +6 -0
  65. package/template/overlays/db-sqlite/backend/drizzle.config.ts +11 -0
  66. package/template/overlays/db-sqlite/backend/scripts/db-check.js +20 -0
  67. package/template/overlays/db-sqlite/backend/src/infra/db/index.ts +8 -0
  68. package/template/overlays/id-serial/packages/shared/src/schemas/user.schema.ts +14 -0
  69. package/template/overlays/id-uuid/packages/shared/src/schemas/user.schema.ts +14 -0
  70. package/template/overlays/models/postgres-serial/backend/src/domains/auth/auth.model.ts +12 -0
  71. package/template/overlays/models/postgres-serial/backend/src/domains/user/user.model.ts +10 -0
  72. package/template/overlays/models/postgres-uuid/backend/src/domains/auth/auth.model.ts +15 -0
  73. package/template/overlays/models/postgres-uuid/backend/src/domains/user/user.model.ts +13 -0
  74. package/template/overlays/models/sqlite-serial/backend/src/domains/auth/auth.model.ts +14 -0
  75. package/template/overlays/models/sqlite-serial/backend/src/domains/user/user.model.ts +12 -0
  76. package/template/overlays/models/sqlite-uuid/backend/src/domains/auth/auth.model.ts +17 -0
  77. package/template/overlays/models/sqlite-uuid/backend/src/domains/user/user.model.ts +15 -0
  78. package/template/backend/src/domains/user/user.routes.test.ts +0 -82
  79. package/template/backend/src/infra/middlewares/require-auth.test.ts +0 -51
  80. package/template/backend/src/infra/middlewares/require-auth.ts +0 -36
  81. package/template/frontend/AGENTS.md +0 -5
  82. package/template/frontend/next.config.ts +0 -7
  83. package/template/package.json +0 -17
  84. package/template/pnpm-workspace.yaml +0 -4
  85. package/template/{backend → base/backend}/.env.example +0 -0
  86. package/template/{backend → base/backend}/README.md +0 -0
  87. package/template/{backend → base/backend}/api/index.ts +0 -0
  88. package/template/{backend → base/backend}/drizzle/0000_grey_lethal_legion.sql +0 -0
  89. package/template/{backend → base/backend}/drizzle/meta/0000_snapshot.json +0 -0
  90. package/template/{backend → base/backend}/drizzle/meta/_journal.json +0 -0
  91. package/template/{backend → base/backend}/drizzle.config.ts +0 -0
  92. package/template/{backend → base/backend}/gitignore +0 -0
  93. package/template/{backend → base/backend}/public/.gitkeep +0 -0
  94. package/template/{backend → base/backend}/scripts/db-check.js +0 -0
  95. package/template/{backend → base/backend}/src/config/.gitkeep +0 -0
  96. package/template/{backend → base/backend}/src/domains/auth/.gitkeep +0 -0
  97. package/template/{backend → base/backend}/src/domains/user/.gitkeep +0 -0
  98. package/template/{backend → base/backend}/src/index.ts +1 -1
  99. package/template/{backend → base/backend}/src/infra/db/.gitkeep +0 -0
  100. package/template/{backend → base/backend}/src/infra/db/index.ts +0 -0
  101. package/template/{backend → base/backend}/src/infra/middlewares/.gitkeep +0 -0
  102. package/template/{backend → base/backend}/vercel.json +0 -0
  103. package/template/{frontend → base/frontend}/.env.example +0 -0
  104. package/template/{frontend → base/frontend}/CLAUDE.md +0 -0
  105. package/template/{frontend → base/frontend}/README.md +0 -0
  106. package/template/{frontend → base/frontend}/app/dashboard/page.tsx +2 -2
  107. package/template/{frontend → base/frontend}/app/icon.png +0 -0
  108. package/template/{frontend → base/frontend}/components/require-auth.tsx +1 -1
  109. package/template/{frontend → base/frontend}/components.json +0 -0
  110. package/template/{frontend → base/frontend}/domains/auth/components/.gitkeep +0 -0
  111. package/template/{frontend → base/frontend}/domains/auth/hooks/.gitkeep +0 -0
  112. package/template/{frontend → base/frontend}/domains/auth/services/.gitkeep +0 -0
  113. package/template/{frontend → base/frontend}/domains/user/components/.gitkeep +0 -0
  114. package/template/{frontend → base/frontend}/domains/user/hooks/use-profile.ts +2 -2
  115. /package/template/{frontend → base/frontend}/eslint.config.mjs +0 -0
  116. /package/template/{frontend → base/frontend}/gitignore +0 -0
  117. /package/template/{frontend → base/frontend}/postcss.config.mjs +0 -0
  118. /package/template/{frontend → base/frontend}/public/favicon.ico +0 -0
  119. /package/template/{frontend → base/frontend}/public/logo.png +0 -0
@@ -4,9 +4,9 @@ import { csrf } from "hono/csrf"
4
4
  import { logger as honoLogger } from "hono/logger"
5
5
  import { requestId } from "hono/request-id"
6
6
  import { env } from "./config/env.js"
7
- import { logger, type AppVariables } from "./utils/logger.js"
8
7
  import { authRoutes } from "./domains/auth/auth.routes.js"
9
8
  import { userRoutes } from "./domains/user/user.routes.js"
9
+ import { type AppVariables, logger } from "./utils/logger.js"
10
10
 
11
11
  /**
12
12
  * Hono application instance — routes, middleware, everything except the transport.
@@ -16,9 +16,7 @@ import { userRoutes } from "./domains/user/user.routes.js"
16
16
  */
17
17
  export const app = new Hono<{ Variables: AppVariables }>()
18
18
 
19
- const allowedOrigins = env.FRONTEND_URL
20
- .split(",")
21
- .map((url) => url.trim())
19
+ const allowedOrigins = env.FRONTEND_URL.split(",").map((url) => url.trim())
22
20
 
23
21
  // 1. Generate unique request IDs for all API calls
24
22
  app.use("/api/*", requestId())
@@ -43,7 +41,7 @@ app.use(
43
41
  "/api/*",
44
42
  honoLogger((...args) => {
45
43
  logger.info(args.join(" "))
46
- })
44
+ }),
47
45
  )
48
46
 
49
47
  app.use(
@@ -51,21 +49,18 @@ app.use(
51
49
  cors({
52
50
  origin: allowedOrigins,
53
51
  credentials: true,
54
- })
52
+ }),
55
53
  )
56
54
 
57
55
  app.use(
58
56
  "/api/*",
59
57
  csrf({
60
58
  origin: allowedOrigins,
61
- })
59
+ }),
62
60
  )
63
61
 
64
- const routes = app
65
- .route("/api/auth", authRoutes)
66
- .route("/api/user", userRoutes)
62
+ const routes = app.route("/api/auth", authRoutes).route("/api/user", userRoutes)
67
63
 
68
64
  app.get("/", (c) => c.json({ status: "ok" }))
69
65
 
70
66
  export type AppType = typeof routes
71
-
@@ -0,0 +1,91 @@
1
+ import { Hono } from "hono"
2
+ import { SignJWT } from "jose"
3
+ import { describe, expect, it } from "vitest"
4
+ import { env } from "../../config/env.js"
5
+ import type { AuthVariables } from "./require-auth.js"
6
+ import { requireAuth } from "./require-auth.js"
7
+
8
+ const secret = new TextEncoder().encode(env.JWT_SECRET)
9
+
10
+ const signTestToken = (payload: Record<string, unknown>, expiresIn = "15m") =>
11
+ new SignJWT(payload)
12
+ .setProtectedHeader({ alg: "HS256" })
13
+ .setExpirationTime(expiresIn)
14
+ .sign(secret)
15
+
16
+ function buildApp() {
17
+ const app = new Hono<{ Variables: AuthVariables }>()
18
+ app.use("/protected", requireAuth)
19
+ app.get("/protected", (c) => c.json({ user: c.get("user"), authMethod: c.get("authMethod") }))
20
+ return app
21
+ }
22
+
23
+ describe("requireAuth", () => {
24
+ it("returns 401 when no token is present in cookie or header", async () => {
25
+ const res = await buildApp().request("/protected")
26
+ expect(res.status).toBe(401)
27
+ })
28
+
29
+ it("returns 401 when the token is garbage", async () => {
30
+ const res = await buildApp().request("/protected", {
31
+ headers: { Cookie: "access_token=not-a-real-token" },
32
+ })
33
+ expect(res.status).toBe(401)
34
+ })
35
+
36
+ it("returns 401 when the token is expired", async () => {
37
+ const expired = await signTestToken({ sub: "user-1", email: "a@b.com", role: "user" }, "-1s")
38
+ const res = await buildApp().request("/protected", {
39
+ headers: { Cookie: `access_token=${expired}` },
40
+ })
41
+ expect(res.status).toBe(401)
42
+ })
43
+
44
+ it("calls next() and exposes payload with authMethod='cookie' when cookie token is valid", async () => {
45
+ const token = await signTestToken({ sub: "user-1", email: "a@b.com", role: "user" })
46
+ const res = await buildApp().request("/protected", {
47
+ headers: { Cookie: `access_token=${token}` },
48
+ })
49
+ expect(res.status).toBe(200)
50
+ const body = await res.json()
51
+ expect(body.user.sub).toBe("user-1")
52
+ expect(body.user.email).toBe("a@b.com")
53
+ expect(body.authMethod).toBe("cookie")
54
+ })
55
+
56
+ it("calls next() and exposes payload with authMethod='bearer' when Authorization header is valid", async () => {
57
+ const token = await signTestToken({ sub: "user-2", email: "b@b.com", role: "admin" })
58
+ const res = await buildApp().request("/protected", {
59
+ headers: { Authorization: `Bearer ${token}` },
60
+ })
61
+ expect(res.status).toBe(200)
62
+ const body = await res.json()
63
+ expect(body.user.sub).toBe("user-2")
64
+ expect(body.user.email).toBe("b@b.com")
65
+ expect(body.user.role).toBe("admin")
66
+ expect(body.authMethod).toBe("bearer")
67
+ })
68
+
69
+ it("prefers Bearer token over cookie when both are supplied", async () => {
70
+ const cookieToken = await signTestToken({
71
+ sub: "user-cookie",
72
+ email: "cookie@b.com",
73
+ role: "user",
74
+ })
75
+ const bearerToken = await signTestToken({
76
+ sub: "user-bearer",
77
+ email: "bearer@b.com",
78
+ role: "admin",
79
+ })
80
+ const res = await buildApp().request("/protected", {
81
+ headers: {
82
+ Cookie: `access_token=${cookieToken}`,
83
+ Authorization: `Bearer ${bearerToken}`,
84
+ },
85
+ })
86
+ expect(res.status).toBe(200)
87
+ const body = await res.json()
88
+ expect(body.user.sub).toBe("user-bearer")
89
+ expect(body.authMethod).toBe("bearer")
90
+ })
91
+ })
@@ -0,0 +1,63 @@
1
+ import type { MiddlewareHandler } from "hono"
2
+ import { getCookie } from "hono/cookie"
3
+ import { authService } from "../../domains/auth/auth.service.js"
4
+ import type { AppVariables } from "../../utils/logger.js"
5
+
6
+ export type AuthMethod = "bearer" | "cookie"
7
+
8
+ export interface AuthVariables extends AppVariables {
9
+ authMethod?: AuthMethod
10
+ }
11
+
12
+ /**
13
+ * Authentication guard middleware.
14
+ * Enforces the presence and validity of the short-lived JWT access token
15
+ * from Authorization: Bearer <token> header or request cookies (Hybrid Auth).
16
+ * Resolves the payload, binds it to Hono context variables (`c.get("user")`),
17
+ * and attaches the authenticated userId and authMethod to the request logger context.
18
+ *
19
+ * Usage:
20
+ * Apply to any endpoint requiring authentication (e.g. `userRoutes.get("/me", requireAuth, ...)`).
21
+ */
22
+ export const requireAuth: MiddlewareHandler<{ Variables: AuthVariables }> = async (c, next) => {
23
+ let token: string | undefined
24
+ let authMethod: AuthMethod | undefined
25
+
26
+ // 1. Inspect Authorization header (Bearer token) first
27
+ const authHeader = c.req.header("Authorization") || c.req.header("authorization")
28
+ if (authHeader?.toLowerCase().startsWith("bearer ")) {
29
+ token = authHeader.slice(7).trim()
30
+ authMethod = "bearer"
31
+ }
32
+
33
+ // 2. Fall back to access_token cookie
34
+ if (!token) {
35
+ const cookieToken = getCookie(c, "access_token")
36
+ if (cookieToken) {
37
+ token = cookieToken
38
+ authMethod = "cookie"
39
+ }
40
+ }
41
+
42
+ if (!token || !authMethod) {
43
+ return c.json({ error: "Unauthorized" }, 401)
44
+ }
45
+
46
+ try {
47
+ // Decode and verify signature & expiration
48
+ const payload = await authService.verifyAccessToken(token)
49
+ c.set("user", payload)
50
+ c.set("authMethod", authMethod)
51
+
52
+ // Enrich the child logger with the user's ID and authMethod for all downstream actions
53
+ const reqLogger = c.get("logger")
54
+ if (reqLogger) {
55
+ c.set("logger", reqLogger.child({ userId: Number(payload.sub), authMethod }))
56
+ }
57
+ } catch {
58
+ // Return standard 401 response on invalid / expired signature
59
+ return c.json({ error: "Unauthorized" }, 401)
60
+ }
61
+
62
+ return await next()
63
+ }
@@ -0,0 +1,66 @@
1
+ import { Hono } from "hono"
2
+ import { describe, expect, it } from "vitest"
3
+ import type { AppVariables } from "../../utils/logger.js"
4
+ import { requireRole } from "./require-role.js"
5
+
6
+ function buildApp(
7
+ allowedRoles: string[],
8
+ initialUser?: { sub: string; email: string; role: string },
9
+ ) {
10
+ const app = new Hono<{ Variables: AppVariables }>()
11
+
12
+ if (initialUser) {
13
+ app.use("/admin/*", async (c, next) => {
14
+ c.set("user", initialUser)
15
+ await next()
16
+ })
17
+ }
18
+
19
+ app.use("/admin/dashboard", requireRole(...allowedRoles))
20
+ app.get("/admin/dashboard", (c) => c.json({ access: "granted", role: c.get("user")?.role }))
21
+
22
+ return app
23
+ }
24
+
25
+ describe("requireRole middleware", () => {
26
+ it("returns 401 when user is not authenticated in context", async () => {
27
+ const app = buildApp(["admin"])
28
+ const res = await app.request("/admin/dashboard")
29
+
30
+ expect(res.status).toBe(401)
31
+ const body = await res.json()
32
+ expect(body.error).toBe("Unauthorized")
33
+ })
34
+
35
+ it("returns 403 when user role does not match required role", async () => {
36
+ const app = buildApp(["admin"], { sub: "1", email: "user@test.com", role: "user" })
37
+ const res = await app.request("/admin/dashboard")
38
+
39
+ expect(res.status).toBe(403)
40
+ const body = await res.json()
41
+ expect(body.error).toBe("Forbidden: insufficient permissions")
42
+ })
43
+
44
+ it("returns 200 when user has the exact required role", async () => {
45
+ const app = buildApp(["admin"], { sub: "1", email: "admin@test.com", role: "admin" })
46
+ const res = await app.request("/admin/dashboard")
47
+
48
+ expect(res.status).toBe(200)
49
+ const body = await res.json()
50
+ expect(body.access).toBe("granted")
51
+ expect(body.role).toBe("admin")
52
+ })
53
+
54
+ it("returns 200 when user matches one of several permitted roles", async () => {
55
+ const app = buildApp(["editor", "admin", "superadmin"], {
56
+ sub: "2",
57
+ email: "editor@test.com",
58
+ role: "editor",
59
+ })
60
+ const res = await app.request("/admin/dashboard")
61
+
62
+ expect(res.status).toBe(200)
63
+ const body = await res.json()
64
+ expect(body.access).toBe("granted")
65
+ })
66
+ })
@@ -0,0 +1,34 @@
1
+ import type { MiddlewareHandler } from "hono"
2
+ import type { AppVariables } from "../../utils/logger.js"
3
+
4
+ /**
5
+ * Role-Based Access Control (RBAC) guard middleware.
6
+ * Verifies that the authenticated user possesses one of the allowed roles.
7
+ * Must be mounted AFTER `requireAuth` in the middleware chain.
8
+ *
9
+ * @param allowedRoles List of roles permitted to access the route (e.g. "admin", "moderator")
10
+ * @returns 401 if user is not authenticated, 403 if user lacks required role, otherwise calls next()
11
+ */
12
+ export const requireRole = (
13
+ ...allowedRoles: string[]
14
+ ): MiddlewareHandler<{ Variables: AppVariables }> => {
15
+ return async (c, next) => {
16
+ const user = c.get("user")
17
+ if (!user) {
18
+ return c.json({ error: "Unauthorized" }, 401)
19
+ }
20
+
21
+ if (!allowedRoles.includes(user.role)) {
22
+ const reqLogger = c.get("logger")
23
+ if (reqLogger) {
24
+ reqLogger.warn(
25
+ { userId: user.sub, userRole: user.role, requiredRoles: allowedRoles },
26
+ "Access forbidden: insufficient role permissions",
27
+ )
28
+ }
29
+ return c.json({ error: "Forbidden: insufficient permissions" }, 403)
30
+ }
31
+
32
+ return await next()
33
+ }
34
+ }
@@ -14,23 +14,20 @@ if (env.LOG_FILE_PATH) {
14
14
  // If a log file path is provided, stream to both stdout (console) and the file
15
15
  const streams = [
16
16
  { stream: process.stdout },
17
- { stream: pino.destination({ dest: env.LOG_FILE_PATH, sync: false }) }
17
+ { stream: pino.destination({ dest: env.LOG_FILE_PATH, sync: false }) },
18
18
  ]
19
19
  loggerInstance = pino({ level }, pino.multistream(streams))
20
20
  } else {
21
21
  // Otherwise default to standard stdout (with pretty formatting in local development)
22
- loggerInstance = pino({
23
- level,
24
- transport:
25
- env.NODE_ENV === "production"
26
- ? undefined
27
- : { target: "pino-pretty", options: { colorize: true } },
28
- })
22
+ const options: pino.LoggerOptions = { level }
23
+ if (env.NODE_ENV !== "production") {
24
+ options.transport = { target: "pino-pretty", options: { colorize: true } }
25
+ }
26
+ loggerInstance = pino(options)
29
27
  }
30
28
 
31
29
  export const logger = loggerInstance
32
30
 
33
-
34
31
  import type { Logger } from "pino"
35
32
  import type { AuthTokenPayload } from "../domains/auth/auth.types.js"
36
33
 
@@ -38,5 +35,5 @@ export type AppVariables = {
38
35
  requestId: string
39
36
  logger: Logger
40
37
  user?: AuthTokenPayload
38
+ authMethod?: "bearer" | "cookie"
41
39
  }
42
-
@@ -0,0 +1,37 @@
1
+ import { isUuidv7, uuidv7 } from "@restack/shared"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ describe("uuidv7 generator", () => {
5
+ it("generates a valid UUIDv7 string", () => {
6
+ const id = uuidv7()
7
+ expect(isUuidv7(id)).toBe(true)
8
+ })
9
+
10
+ it("conforms to version 7 and RFC 9562 variant format", () => {
11
+ const id = uuidv7()
12
+ const parts = id.split("-")
13
+ expect(parts).toHaveLength(5)
14
+ // 3rd part starts with '7' (version 7)
15
+ expect(parts[2]?.charAt(0)).toBe("7")
16
+ // 4th part starts with 8, 9, a, or b (variant 1)
17
+ expect(["8", "9", "a", "b"]).toContain(parts[3]?.charAt(0).toLowerCase())
18
+ })
19
+
20
+ it("produces chronologically ordered identifiers", async () => {
21
+ const ids: string[] = []
22
+ for (let i = 0; i < 20; i++) {
23
+ ids.push(uuidv7())
24
+ // short delay to test monotonic sorting
25
+ await new Promise((r) => setTimeout(r, 2))
26
+ }
27
+
28
+ const sorted = [...ids].sort()
29
+ expect(ids).toEqual(sorted)
30
+ })
31
+
32
+ it("isUuidv7 rejects invalid UUID formats", () => {
33
+ expect(isUuidv7("not-a-uuid")).toBe(false)
34
+ // UUIDv4 (version nibble is 4, not 7)
35
+ expect(isUuidv7("550e8400-e29b-41d4-a716-446655440000")).toBe(false)
36
+ })
37
+ })
@@ -3,6 +3,10 @@
3
3
  "target": "ESNext",
4
4
  "module": "NodeNext",
5
5
  "strict": true,
6
+ "noUncheckedIndexedAccess": true,
7
+ "exactOptionalPropertyTypes": true,
8
+ "noImplicitReturns": true,
9
+ "noFallthroughCasesInSwitch": true,
6
10
  "verbatimModuleSyntax": true,
7
11
  "skipLibCheck": true,
8
12
  "types": [
@@ -0,0 +1,11 @@
1
+ <!-- BEGIN:nextjs-agent-rules -->
2
+ # This is NOT the Next.js you know
3
+
4
+ This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
5
+
6
+ ## Guardrails
7
+ - **Typed Routes Active**: All internal links `<Link href="...">` are strictly checked at compile time. Never use untyped arbitrary string paths.
8
+ - **Typed Environment**: Always import `env` from `@/env` (`@t3-oss/env-nextjs`). Never read raw `process.env`.
9
+ - **Semi-DDD Boundary**: Keep `app/` strictly for routing/metadata only. Delegate all rendering to `domains/[feature]/components`.
10
+ <!-- END:nextjs-agent-rules -->
11
+
@@ -4,7 +4,7 @@ import logo from "@/public/logo.png"
4
4
 
5
5
  /**
6
6
  * Next.js Router Page: Login
7
- *
7
+ *
8
8
  * Semi-DDD rules:
9
9
  * - Files in `frontend/app/` are strictly for layout, routing config, metadata, or entry gates.
10
10
  * - Do not implement custom JSX layouts, forms, state, or complex components here.
@@ -4,7 +4,7 @@ import logo from "@/public/logo.png"
4
4
 
5
5
  /**
6
6
  * Next.js Router Page: Register
7
- *
7
+ *
8
8
  * Semi-DDD rules:
9
9
  * - Files in `frontend/app/` are strictly for layout, routing config, metadata, or entry gates.
10
10
  * - Do not implement custom JSX layouts, forms, state, or complex components here.
@@ -128,4 +128,4 @@
128
128
  html {
129
129
  @apply font-sans;
130
130
  }
131
- }
131
+ }
@@ -1,16 +1,16 @@
1
- import type { Metadata } from "next";
2
- import { Geist, Geist_Mono } from "next/font/google";
3
- import "./globals.css";
1
+ import type { Metadata } from "next"
2
+ import { Geist, Geist_Mono } from "next/font/google"
3
+ import "./globals.css"
4
4
 
5
5
  const geistSans = Geist({
6
6
  variable: "--font-geist-sans",
7
7
  subsets: ["latin"],
8
- });
8
+ })
9
9
 
10
10
  const geistMono = Geist_Mono({
11
11
  variable: "--font-geist-mono",
12
12
  subsets: ["latin"],
13
- });
13
+ })
14
14
 
15
15
  export const metadata: Metadata = {
16
16
  title: "RestackPattern",
@@ -18,19 +18,16 @@ export const metadata: Metadata = {
18
18
  icons: {
19
19
  icon: "/favicon.ico",
20
20
  },
21
- };
21
+ }
22
22
 
23
23
  export default function RootLayout({
24
24
  children,
25
25
  }: Readonly<{
26
- children: React.ReactNode;
26
+ children: React.ReactNode
27
27
  }>) {
28
28
  return (
29
- <html
30
- lang="en"
31
- className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
32
- >
29
+ <html lang="en" className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}>
33
30
  <body className="min-h-full flex flex-col">{children}</body>
34
31
  </html>
35
- );
32
+ )
36
33
  }
@@ -1,11 +1,11 @@
1
- import Link from "next/link"
2
1
  import Image from "next/image"
2
+ import Link from "next/link"
3
3
  import { Button } from "@/components/ui/button"
4
4
  import logo from "@/public/logo.png"
5
5
 
6
6
  /**
7
7
  * Next.js Router Page: Landing Page (root)
8
- *
8
+ *
9
9
  * Semi-DDD rules:
10
10
  * - Files in `frontend/app/` are strictly for layout, routing config, metadata, or entry gates.
11
11
  * - Do not implement custom JSX layouts, forms, state, or complex components here.
@@ -1,10 +1,10 @@
1
- import { Button } from "@/components/ui/button";
1
+ import Link from "next/link"
2
+ import { Button } from "@/components/ui/button"
2
3
  import { ProfileCard } from "@/domains/user/components/profile-card"
3
- import Link from "next/link";
4
4
 
5
5
  /**
6
6
  * Next.js Router Page: Profile
7
- *
7
+ *
8
8
  * Semi-DDD rules:
9
9
  * - Files in `frontend/app/` are strictly for layout, routing config, metadata, or entry gates.
10
10
  * - Do not implement custom JSX layouts, forms, state, or complex components here.
@@ -1,6 +1,6 @@
1
- import * as React from "react"
2
1
  import { cva, type VariantProps } from "class-variance-authority"
3
2
  import { Slot } from "radix-ui"
3
+ import type * as React from "react"
4
4
 
5
5
  import { cn } from "@/lib/utils"
6
6
 
@@ -38,7 +38,7 @@ const buttonVariants = cva(
38
38
  variant: "default",
39
39
  size: "default",
40
40
  },
41
- }
41
+ },
42
42
  )
43
43
 
44
44
  function Button({
@@ -1,4 +1,4 @@
1
- import * as React from "react"
1
+ import type * as React from "react"
2
2
 
3
3
  import { cn } from "@/lib/utils"
4
4
 
@@ -13,7 +13,7 @@ function Card({
13
13
  data-size={size}
14
14
  className={cn(
15
15
  "group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
16
- className
16
+ className,
17
17
  )}
18
18
  {...props}
19
19
  />
@@ -26,7 +26,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
26
26
  data-slot="card-header"
27
27
  className={cn(
28
28
  "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
29
- className
29
+ className,
30
30
  )}
31
31
  {...props}
32
32
  />
@@ -39,7 +39,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
39
39
  data-slot="card-title"
40
40
  className={cn(
41
41
  "font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
42
- className
42
+ className,
43
43
  )}
44
44
  {...props}
45
45
  />
@@ -60,10 +60,7 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
60
60
  return (
61
61
  <div
62
62
  data-slot="card-action"
63
- className={cn(
64
- "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
65
- className
66
- )}
63
+ className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
67
64
  {...props}
68
65
  />
69
66
  )
@@ -71,11 +68,7 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
71
68
 
72
69
  function CardContent({ className, ...props }: React.ComponentProps<"div">) {
73
70
  return (
74
- <div
75
- data-slot="card-content"
76
- className={cn("px-(--card-spacing)", className)}
77
- {...props}
78
- />
71
+ <div data-slot="card-content" className={cn("px-(--card-spacing)", className)} {...props} />
79
72
  )
80
73
  }
81
74
 
@@ -85,19 +78,11 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
85
78
  data-slot="card-footer"
86
79
  className={cn(
87
80
  "flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)",
88
- className
81
+ className,
89
82
  )}
90
83
  {...props}
91
84
  />
92
85
  )
93
86
  }
94
87
 
95
- export {
96
- Card,
97
- CardHeader,
98
- CardFooter,
99
- CardTitle,
100
- CardAction,
101
- CardDescription,
102
- CardContent,
103
- }
88
+ export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
@@ -1,4 +1,4 @@
1
- import * as React from "react"
1
+ import type * as React from "react"
2
2
 
3
3
  import { cn } from "@/lib/utils"
4
4
 
@@ -9,7 +9,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
9
9
  data-slot="input"
10
10
  className={cn(
11
11
  "h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
12
- className
12
+ className,
13
13
  )}
14
14
  {...props}
15
15
  />
@@ -1,20 +1,17 @@
1
1
  "use client"
2
2
 
3
- import * as React from "react"
4
3
  import { Label as LabelPrimitive } from "radix-ui"
4
+ import type * as React from "react"
5
5
 
6
6
  import { cn } from "@/lib/utils"
7
7
 
8
- function Label({
9
- className,
10
- ...props
11
- }: React.ComponentProps<typeof LabelPrimitive.Root>) {
8
+ function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
12
9
  return (
13
10
  <LabelPrimitive.Root
14
11
  data-slot="label"
15
12
  className={cn(
16
13
  "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
17
- className
14
+ className,
18
15
  )}
19
16
  {...props}
20
17
  />