@withmata/blueprints 0.2.0

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 (77) hide show
  1. package/.claude/commands/audit.md +179 -0
  2. package/.claude/commands/discover.md +92 -0
  3. package/.claude/commands/new-blueprint.md +265 -0
  4. package/.claude/commands/new-project.md +230 -0
  5. package/.claude/commands/scaffold-auth.md +310 -0
  6. package/.claude/commands/scaffold-db.md +270 -0
  7. package/.claude/commands/scaffold-foundation.md +158 -0
  8. package/.cursor/commands/audit.md +179 -0
  9. package/.cursor/commands/discover.md +92 -0
  10. package/.cursor/commands/new-blueprint.md +265 -0
  11. package/.cursor/commands/new-project.md +230 -0
  12. package/.cursor/commands/scaffold-auth.md +310 -0
  13. package/.cursor/commands/scaffold-db.md +270 -0
  14. package/.cursor/commands/scaffold-foundation.md +158 -0
  15. package/.opencode/commands/audit.md +183 -0
  16. package/.opencode/commands/discover.md +96 -0
  17. package/.opencode/commands/new-blueprint.md +269 -0
  18. package/.opencode/commands/new-project.md +234 -0
  19. package/.opencode/commands/scaffold-auth.md +314 -0
  20. package/.opencode/commands/scaffold-db.md +274 -0
  21. package/.opencode/commands/scaffold-foundation.md +162 -0
  22. package/ENGINEERING.md +47 -0
  23. package/blueprints/discovery/product-discovery/BLUEPRINT.md +361 -0
  24. package/blueprints/discovery/product-discovery/templates/archetype-template.md +143 -0
  25. package/blueprints/discovery/product-discovery/templates/product-brief.md +65 -0
  26. package/blueprints/discovery/product-discovery/templates/product-thesis.md +64 -0
  27. package/blueprints/discovery/product-discovery/templates/research-summary.md +43 -0
  28. package/blueprints/features/auth-better-auth/BLUEPRINT.md +794 -0
  29. package/blueprints/features/auth-better-auth/files/client/auth-client.ts +31 -0
  30. package/blueprints/features/auth-better-auth/files/client/context/organization.ts +236 -0
  31. package/blueprints/features/auth-better-auth/files/client/hooks/use-create-organization.ts +45 -0
  32. package/blueprints/features/auth-better-auth/files/client/hooks/use-has-permission.ts +26 -0
  33. package/blueprints/features/auth-better-auth/files/client/hooks/use-organization.ts +64 -0
  34. package/blueprints/features/auth-better-auth/files/client/schema/auth.ts +21 -0
  35. package/blueprints/features/auth-better-auth/files/client/schema/organization.ts +51 -0
  36. package/blueprints/features/auth-better-auth/files/client/types/organization.ts +20 -0
  37. package/blueprints/features/auth-better-auth/files/db/auth-schema.ts +184 -0
  38. package/blueprints/features/auth-better-auth/files/db/drizzle.config.ts +21 -0
  39. package/blueprints/features/auth-better-auth/files/middleware/route-protection.ts +84 -0
  40. package/blueprints/features/auth-better-auth/files/server/auth-db.ts +18 -0
  41. package/blueprints/features/auth-better-auth/files/server/auth.ts +159 -0
  42. package/blueprints/features/auth-better-auth/files/server/env.ts +44 -0
  43. package/blueprints/features/auth-better-auth/files/server/middleware.ts +144 -0
  44. package/blueprints/features/db-drizzle-postgres/BLUEPRINT.md +596 -0
  45. package/blueprints/features/db-drizzle-postgres/files/db/drizzle.config.ts +18 -0
  46. package/blueprints/features/db-drizzle-postgres/files/db/env.example +3 -0
  47. package/blueprints/features/db-drizzle-postgres/files/db/package.json +33 -0
  48. package/blueprints/features/db-drizzle-postgres/files/db/src/client.ts +34 -0
  49. package/blueprints/features/db-drizzle-postgres/files/db/src/example-entity.ts +73 -0
  50. package/blueprints/features/db-drizzle-postgres/files/db/src/index.ts +8 -0
  51. package/blueprints/features/db-drizzle-postgres/files/db/src/scripts/seed.ts +50 -0
  52. package/blueprints/features/db-drizzle-postgres/files/db/tsconfig.json +13 -0
  53. package/blueprints/features/tailwind-v4/BLUEPRINT.md +29 -0
  54. package/blueprints/features/ui-shared-components/BLUEPRINT.md +35 -0
  55. package/blueprints/foundation/monorepo-turbo/BLUEPRINT.md +378 -0
  56. package/blueprints/foundation/monorepo-turbo/files/apps/web/app/globals.css +2 -0
  57. package/blueprints/foundation/monorepo-turbo/files/apps/web/app/layout.tsx +23 -0
  58. package/blueprints/foundation/monorepo-turbo/files/apps/web/app/page.tsx +7 -0
  59. package/blueprints/foundation/monorepo-turbo/files/apps/web/env.ts +13 -0
  60. package/blueprints/foundation/monorepo-turbo/files/apps/web/next.config.ts +12 -0
  61. package/blueprints/foundation/monorepo-turbo/files/apps/web/package.json +28 -0
  62. package/blueprints/foundation/monorepo-turbo/files/apps/web/postcss.config.mjs +5 -0
  63. package/blueprints/foundation/monorepo-turbo/files/apps/web/tsconfig.json +18 -0
  64. package/blueprints/foundation/monorepo-turbo/files/config/tailwind-config/package.json +14 -0
  65. package/blueprints/foundation/monorepo-turbo/files/config/tailwind-config/postcss.config.mjs +5 -0
  66. package/blueprints/foundation/monorepo-turbo/files/config/tailwind-config/shared-styles.css +88 -0
  67. package/blueprints/foundation/monorepo-turbo/files/config/typescript-config/base.json +19 -0
  68. package/blueprints/foundation/monorepo-turbo/files/config/typescript-config/nextjs.json +12 -0
  69. package/blueprints/foundation/monorepo-turbo/files/config/typescript-config/package.json +5 -0
  70. package/blueprints/foundation/monorepo-turbo/files/config/typescript-config/react-library.json +7 -0
  71. package/blueprints/foundation/monorepo-turbo/files/root/biome.json +46 -0
  72. package/blueprints/foundation/monorepo-turbo/files/root/gitignore +35 -0
  73. package/blueprints/foundation/monorepo-turbo/files/root/package.json +25 -0
  74. package/blueprints/foundation/monorepo-turbo/files/root/pnpm-workspace.yaml +5 -0
  75. package/blueprints/foundation/monorepo-turbo/files/root/turbo.json +30 -0
  76. package/dist/index.js +453 -0
  77. package/package.json +53 -0
@@ -0,0 +1,21 @@
1
+ // =============================================================================
2
+ // Drizzle Kit Config — Auth Database
3
+ // =============================================================================
4
+ // Used by drizzle-kit to generate and run migrations for the auth database.
5
+ //
6
+ // Place this in your DB package: packages/db/drizzle/auth/drizzle.config.ts
7
+ // =============================================================================
8
+
9
+ import { config } from "dotenv";
10
+ import { defineConfig } from "drizzle-kit";
11
+
12
+ config({ path: "./.env" });
13
+
14
+ const AUTH_DATABASE_URL = process.env.AUTH_DATABASE_URL!;
15
+
16
+ export default defineConfig({
17
+ schema: "./src/auth/schema.ts",
18
+ out: "./drizzle/auth",
19
+ dialect: "postgresql", // CONFIGURE: "postgresql" | "mysql" | "sqlite"
20
+ dbCredentials: { url: AUTH_DATABASE_URL },
21
+ });
@@ -0,0 +1,84 @@
1
+ // =============================================================================
2
+ // Next.js Middleware — Route Protection
3
+ // =============================================================================
4
+ // Protects routes by checking for the Better Auth session cookie.
5
+ // This is a COOKIE-PRESENCE CHECK only — actual session validation happens
6
+ // server-side in the auth middleware.
7
+ //
8
+ // Route classification:
9
+ // - Public: /auth/* — accessible without auth
10
+ // - Protected: everything else — redirects to /auth if no session cookie
11
+ // - Onboarding: /onboarding/* — accessible if authenticated
12
+ //
13
+ // Usage: Export this as middleware.ts in your Next.js app root, or integrate
14
+ // into your existing middleware.
15
+ // =============================================================================
16
+
17
+ import type { NextRequest } from "next/server";
18
+ import { NextResponse } from "next/server";
19
+
20
+ // CONFIGURE: adjust these route lists for your app
21
+ const publicRoutes = ["/auth", "/auth/"];
22
+ const publicPrefixes = ["/auth/"];
23
+
24
+ const onboardingRoutes = ["/onboarding", "/onboarding/"];
25
+ const onboardingPrefixes = ["/onboarding/"];
26
+
27
+ function isPublicRoute(pathname: string): boolean {
28
+ if (publicRoutes.includes(pathname)) return true;
29
+ return publicPrefixes.some((prefix) => pathname.startsWith(prefix));
30
+ }
31
+
32
+ function isOnboardingRoute(pathname: string): boolean {
33
+ if (onboardingRoutes.includes(pathname)) return true;
34
+ return onboardingPrefixes.some((prefix) => pathname.startsWith(prefix));
35
+ }
36
+
37
+ export async function middleware(request: NextRequest) {
38
+ const { pathname } = request.nextUrl;
39
+
40
+ // Skip for static files and API routes
41
+ if (
42
+ pathname.startsWith("/_next") ||
43
+ pathname.startsWith("/api") ||
44
+ pathname.includes(".")
45
+ ) {
46
+ return NextResponse.next();
47
+ }
48
+
49
+ // Check for Better Auth session cookie
50
+ const sessionToken =
51
+ request.cookies.get("better-auth.session_token")?.value ||
52
+ request.cookies.get("__Secure-better-auth.session_token")?.value;
53
+
54
+ const isAuthenticated = !!sessionToken;
55
+
56
+ // Public routes — allow access
57
+ if (isPublicRoute(pathname)) {
58
+ // If authenticated and on /auth (exact), redirect to home
59
+ if (isAuthenticated && pathname === "/auth") {
60
+ return NextResponse.redirect(new URL("/", request.url));
61
+ }
62
+ return NextResponse.next();
63
+ }
64
+
65
+ // Protected routes — require auth
66
+ if (!isAuthenticated) {
67
+ const signInUrl = new URL("/auth", request.url);
68
+ signInUrl.searchParams.set("returnUrl", pathname);
69
+ return NextResponse.redirect(signInUrl);
70
+ }
71
+
72
+ // Onboarding routes — allow for authenticated users
73
+ if (isOnboardingRoute(pathname)) {
74
+ return NextResponse.next();
75
+ }
76
+
77
+ return NextResponse.next();
78
+ }
79
+
80
+ export const config = {
81
+ matcher: [
82
+ "/((?!api|_next/static|_next/image|favicon.ico).*)",
83
+ ],
84
+ };
@@ -0,0 +1,18 @@
1
+ // =============================================================================
2
+ // Auth Database Connection
3
+ // =============================================================================
4
+ // Creates a Drizzle ORM instance connected to the auth-specific PostgreSQL
5
+ // database. This is separate from the app's core database.
6
+ //
7
+ // CONFIGURE: If using a different database driver (e.g. @neondatabase/serverless),
8
+ // swap the import and connection setup accordingly.
9
+ // =============================================================================
10
+
11
+ import * as schema from "{{SCOPE}}/db/auth"; // CONFIGURE: your DB package path
12
+ import { drizzle } from "drizzle-orm/node-postgres";
13
+ import pg from "pg";
14
+ import { env } from "./env.js";
15
+
16
+ const pool = new pg.Pool({ connectionString: env.AUTH_DATABASE_URL });
17
+
18
+ export const authDb = drizzle(pool, { schema });
@@ -0,0 +1,159 @@
1
+ // =============================================================================
2
+ // Better Auth Server Configuration
3
+ // =============================================================================
4
+ // This is the core auth configuration. It is FRAMEWORK-AGNOSTIC — mount it on
5
+ // Hono, Next.js API routes, Express, Fastify, or any supported platform.
6
+ // See BLUEPRINT.md > Platform Integration for mounting instructions.
7
+ //
8
+ // Source: extracted from a production Turborepo monorepo.
9
+ // =============================================================================
10
+
11
+ import { betterAuth } from "better-auth";
12
+ import { drizzleAdapter } from "better-auth/adapters/drizzle";
13
+ import { openAPI } from "better-auth/plugins";
14
+ import { organization } from "better-auth/plugins/organization";
15
+ import { Resend } from "resend"; // CONFIGURE: swap email provider if needed
16
+ import { authDb } from "./auth-db.js";
17
+ import { env } from "./env.js";
18
+
19
+ // CONFIGURE: Replace with your email provider
20
+ const resend = new Resend(env.RESEND_API_KEY);
21
+
22
+ // Environment detection for local dev cookie fix
23
+ const isLocalDev = env.FRONTEND_DOMAIN.includes("localhost");
24
+
25
+ const auth = betterAuth({
26
+ // CORS: Which origins can make auth requests
27
+ trustedOrigins: [env.FRONTEND_DOMAIN],
28
+
29
+ emailAndPassword: {
30
+ enabled: true,
31
+ requireEmailVerification: true,
32
+ sendResetPassword: async ({ user, token }) => {
33
+ const resetUrl = `${env.FRONTEND_DOMAIN}/auth/reset-password/${token}`;
34
+ try {
35
+ await resend.emails.send({
36
+ from: "{{APP_NAME}} <{{EMAIL_FROM}}>", // CONFIGURE: your app name and email
37
+ to: user.email,
38
+ subject: "Reset your password",
39
+ html: `
40
+ <div style="font-family: sans-serif; max-width: 480px; margin: 0 auto; padding: 24px;">
41
+ <h2>Reset your password</h2>
42
+ <p>We received a request to reset the password for your {{APP_NAME}} account.</p>
43
+ <a href="${resetUrl}" style="display: inline-block; padding: 12px 24px; background-color: #000; color: #fff; text-decoration: none; border-radius: 6px; margin-top: 16px;">
44
+ Reset Password
45
+ </a>
46
+ <p style="margin-top: 24px; font-size: 14px; color: #666;">
47
+ This link expires in 1 hour. If you didn't request this, you can safely ignore this email.
48
+ </p>
49
+ <p style="margin-top: 16px; font-size: 14px; color: #666;">
50
+ Or copy and paste this link into your browser:<br/>
51
+ <a href="${resetUrl}">${resetUrl}</a>
52
+ </p>
53
+ </div>
54
+ `,
55
+ });
56
+ } catch (error) {
57
+ console.error("Failed to send reset password email:", error);
58
+ }
59
+ },
60
+ },
61
+
62
+ // CONFIGURE: Add/remove social providers as needed
63
+ // See: https://www.better-auth.com/docs/authentication/social-sign-in
64
+ socialProviders: {
65
+ google: {
66
+ clientId: env.GOOGLE_CLIENT_ID,
67
+ clientSecret: env.GOOGLE_CLIENT_SECRET,
68
+ },
69
+ },
70
+
71
+ // Database adapter — uses Drizzle with PostgreSQL
72
+ database: drizzleAdapter(authDb, {
73
+ provider: "pg", // CONFIGURE: "pg" | "mysql" | "sqlite"
74
+ }),
75
+
76
+ // Account linking: merge accounts when user signs in with different providers
77
+ account: {
78
+ accountLinking: {
79
+ enabled: true,
80
+ },
81
+ },
82
+
83
+ // Session configuration
84
+ session: {
85
+ expiresIn: 60 * 60 * 24 * 30, // CONFIGURE: 30 days default
86
+ updateAge: 60 * 60 * 24, // Refresh session every 24 hours
87
+ cookieCache: {
88
+ enabled: true,
89
+ maxAge: 60 * 5, // 5-minute cookie cache avoids DB round-trip per request
90
+ },
91
+ },
92
+
93
+ // CONFIGURE: Add custom fields to the user model
94
+ user: {
95
+ additionalFields: {
96
+ currentOrganizationId: {
97
+ type: "string",
98
+ required: false,
99
+ input: false, // Server-managed, not settable via signup
100
+ },
101
+ },
102
+ },
103
+
104
+ // CONFIGURE: Add/remove plugins
105
+ plugins: [
106
+ openAPI(), // Optional: exposes OpenAPI spec at /api/auth/reference
107
+ // CONFIGURE: Remove organization() if you don't need multi-tenancy
108
+ organization({
109
+ allowUserToCreateOrganization: true,
110
+ organizationLimit: 10, // CONFIGURE: max orgs per user
111
+ creatorRole: "owner",
112
+ membershipLimit: 100, // CONFIGURE: max members per org
113
+ disableOrganizationDeletion: true,
114
+ sendInvitationEmail: async (data) => {
115
+ const inviteUrl = `${env.FRONTEND_DOMAIN}/auth/invite/${data.id}?email=${encodeURIComponent(data.email)}`;
116
+ const inviterName = data.inviter.user.name;
117
+ const orgName = data.organization.name;
118
+
119
+ try {
120
+ await resend.emails.send({
121
+ from: "{{APP_NAME}} <{{EMAIL_FROM}}>", // CONFIGURE: your app name and email
122
+ to: data.email,
123
+ subject: `${inviterName} invited you to join ${orgName}`,
124
+ html: `
125
+ <div style="font-family: sans-serif; max-width: 480px; margin: 0 auto; padding: 24px;">
126
+ <h2>You're invited!</h2>
127
+ <p><strong>${inviterName}</strong> has invited you to join <strong>${orgName}</strong> on {{APP_NAME}}.</p>
128
+ <a href="${inviteUrl}" style="display: inline-block; padding: 12px 24px; background-color: #000; color: #fff; text-decoration: none; border-radius: 6px; margin-top: 16px;">
129
+ Accept Invitation
130
+ </a>
131
+ <p style="margin-top: 24px; font-size: 14px; color: #666;">
132
+ Or copy and paste this link into your browser:<br/>
133
+ <a href="${inviteUrl}">${inviteUrl}</a>
134
+ </p>
135
+ </div>
136
+ `,
137
+ });
138
+ } catch (error) {
139
+ console.error("Failed to send invitation email:", error);
140
+ }
141
+ },
142
+ }),
143
+ ],
144
+
145
+ // Local dev: share cookies across localhost ports (e.g. frontend:3000, backend:4000)
146
+ // In production with proper domain setup, this isn't needed.
147
+ ...(isLocalDev && {
148
+ advanced: {
149
+ crossSubDomainCookies: {
150
+ enabled: true,
151
+ domain: "localhost",
152
+ },
153
+ },
154
+ }),
155
+ });
156
+
157
+ export type Auth = typeof auth;
158
+
159
+ export default auth;
@@ -0,0 +1,44 @@
1
+ // =============================================================================
2
+ // Auth Environment Variables — Server-Side
3
+ // =============================================================================
4
+ // Validates auth-related environment variables at startup using t3-env + Zod.
5
+ //
6
+ // CONFIGURE: This is a RECOMMENDED pattern. If you prefer a different env
7
+ // validation approach, adapt accordingly. The key requirement is that all
8
+ // auth env vars are validated before the server starts.
9
+ //
10
+ // NOTE: This file only includes auth-related variables. Your project will
11
+ // likely have additional env vars — merge them into your project's env.ts.
12
+ // =============================================================================
13
+
14
+ import "dotenv/config";
15
+ import { createEnv } from "@t3-oss/env-core";
16
+ import { z } from "zod";
17
+
18
+ export const env = createEnv({
19
+ server: {
20
+ // Server
21
+ PORT: z.preprocess(
22
+ (str) => parseInt(String(str), 10) || undefined,
23
+ z.number().min(1),
24
+ ),
25
+
26
+ // Auth
27
+ FRONTEND_DOMAIN: z.url(), // Frontend URL for CORS, email links, trusted origins
28
+ BETTER_AUTH_URL: z.url(), // Base URL where auth API is accessible (OAuth callback target)
29
+ BETTER_AUTH_SECRET: z.string().min(1), // Generate with: openssl rand -base64 32
30
+
31
+ // Database
32
+ AUTH_DATABASE_URL: z.url(), // PostgreSQL connection URL for auth database
33
+
34
+ // Social providers — CONFIGURE: add/remove as needed
35
+ GOOGLE_CLIENT_ID: z.string().min(1),
36
+ GOOGLE_CLIENT_SECRET: z.string().min(1),
37
+
38
+ // Email — CONFIGURE: swap if using a different email provider
39
+ RESEND_API_KEY: z.string().min(1),
40
+ },
41
+
42
+ runtimeEnv: process.env,
43
+ emptyStringAsUndefined: true,
44
+ });
@@ -0,0 +1,144 @@
1
+ // =============================================================================
2
+ // Auth Middleware — Hono Reference Implementation
3
+ // =============================================================================
4
+ // This file provides two middleware functions for protecting API routes:
5
+ // - requireAuth() — validates session, attaches user to context
6
+ // - requireOrganization() — validates session + active org membership
7
+ //
8
+ // FRAMEWORK NOTE: This is a Hono implementation. The core pattern is
9
+ // framework-agnostic — `auth.api.getSession({ headers })` works anywhere.
10
+ // Adapt the middleware wrapper for your framework. See BLUEPRINT.md > Platform
11
+ // Integration for examples with Next.js, Express, etc.
12
+ // =============================================================================
13
+
14
+ import type { Context, MiddlewareHandler, Next } from "hono";
15
+ import auth from "./auth.js";
16
+
17
+ // ============================================================================
18
+ // Types
19
+ // ============================================================================
20
+
21
+ export type SessionUser = {
22
+ id: string;
23
+ email: string;
24
+ name: string;
25
+ emailVerified: boolean;
26
+ image?: string | null;
27
+ currentOrganizationId?: string | null;
28
+ createdAt: Date;
29
+ updatedAt: Date;
30
+ };
31
+
32
+ export type Session = {
33
+ id: string;
34
+ userId: string;
35
+ token: string;
36
+ expiresAt: Date;
37
+ createdAt: Date;
38
+ updatedAt: Date;
39
+ ipAddress?: string | null;
40
+ userAgent?: string | null;
41
+ activeOrganizationId?: string | null;
42
+ };
43
+
44
+ export type AuthContext = {
45
+ user: SessionUser;
46
+ session: Session;
47
+ };
48
+
49
+ // ============================================================================
50
+ // Middleware
51
+ // ============================================================================
52
+
53
+ /**
54
+ * Validates the session and attaches user info to context.
55
+ * Returns 401 if no valid session is found.
56
+ */
57
+ export function requireAuth(): MiddlewareHandler<{
58
+ Variables: { auth: AuthContext };
59
+ }> {
60
+ return async (c: Context, next: Next) => {
61
+ try {
62
+ // Framework-agnostic core: auth.api.getSession({ headers })
63
+ const sessionData = await auth.api.getSession({
64
+ headers: c.req.raw.headers,
65
+ });
66
+
67
+ if (!sessionData || !sessionData.user) {
68
+ return c.json(
69
+ { error: "Unauthorized", message: "Valid session required" },
70
+ 401,
71
+ );
72
+ }
73
+
74
+ c.set("auth", {
75
+ user: sessionData.user as SessionUser,
76
+ session: sessionData.session as Session,
77
+ });
78
+
79
+ await next();
80
+ } catch (error) {
81
+ console.error("Auth middleware error:", error);
82
+ return c.json(
83
+ { error: "Unauthorized", message: "Session validation failed" },
84
+ 401,
85
+ );
86
+ }
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Validates session AND checks for active organization.
92
+ * Returns 401 if no session, 403 if no active organization.
93
+ *
94
+ * CONFIGURE: Remove this if not using the organization plugin.
95
+ */
96
+ export function requireOrganization(): MiddlewareHandler<{
97
+ Variables: { auth: AuthContext; organizationId: string };
98
+ }> {
99
+ return async (c: Context, next: Next) => {
100
+ try {
101
+ const sessionData = await auth.api.getSession({
102
+ headers: c.req.raw.headers,
103
+ });
104
+
105
+ if (!sessionData || !sessionData.user) {
106
+ return c.json(
107
+ { error: "Unauthorized", message: "Valid session required" },
108
+ 401,
109
+ );
110
+ }
111
+
112
+ const organizationId = sessionData.session.activeOrganizationId;
113
+
114
+ if (!organizationId) {
115
+ return c.json(
116
+ { error: "Forbidden", message: "Active organization required" },
117
+ 403,
118
+ );
119
+ }
120
+
121
+ c.set("auth", {
122
+ user: sessionData.user as SessionUser,
123
+ session: sessionData.session as Session,
124
+ });
125
+ c.set("organizationId", organizationId);
126
+
127
+ await next();
128
+ } catch (error) {
129
+ console.error("Auth middleware error:", error);
130
+ return c.json(
131
+ { error: "Unauthorized", message: "Session validation failed" },
132
+ 401,
133
+ );
134
+ }
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Helper to extract auth context from Hono context in route handlers.
140
+ * Assumes requireAuth() middleware has already run.
141
+ */
142
+ export function getAuthContext(c: Context): AuthContext {
143
+ return c.get("auth") as AuthContext;
144
+ }