tova 0.9.4 → 0.9.6

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/tova.js CHANGED
@@ -1972,6 +1972,620 @@ ${inlineClient}
1972
1972
 
1973
1973
  // ─── Template Definitions ────────────────────────────────────
1974
1974
 
1975
+ // ─── Auth template content (fullstack + auth) ───────────────────────
1976
+ function fullstackAuthContent(name) {
1977
+ return `// ${name} — Built with Tova
1978
+ // Full-stack app with authentication and security
1979
+
1980
+ shared {
1981
+ type User {
1982
+ id: String
1983
+ email: String
1984
+ role: String
1985
+ }
1986
+ }
1987
+
1988
+ security {
1989
+ cors {
1990
+ origins: ["http://localhost:3000"]
1991
+ methods: ["GET", "POST", "PUT", "DELETE"]
1992
+ credentials: true
1993
+ }
1994
+
1995
+ csrf {
1996
+ enabled: true
1997
+ }
1998
+
1999
+ rate_limit {
2000
+ window: 60
2001
+ max: 100
2002
+ }
2003
+
2004
+ csp {
2005
+ default_src: ["self"]
2006
+ script_src: ["self", "https://cdn.tailwindcss.com"]
2007
+ style_src: ["self", "unsafe-inline"]
2008
+ img_src: ["self", "data:", "https:"]
2009
+ connect_src: ["self"]
2010
+ }
2011
+ }
2012
+
2013
+ auth {
2014
+ secret: env("AUTH_SECRET")
2015
+ token_expires: 900
2016
+ refresh_expires: 604800
2017
+ storage: "cookie"
2018
+
2019
+ provider email {
2020
+ confirm_email: true
2021
+ password_min: 8
2022
+ max_attempts: 5
2023
+ lockout_duration: 900
2024
+ }
2025
+
2026
+ on signup fn(user) {
2027
+ print("New user signed up: " + user.email)
2028
+ }
2029
+
2030
+ on login fn(user) {
2031
+ print("User logged in: " + user.email)
2032
+ }
2033
+
2034
+ on logout fn(user) {
2035
+ print("User logged out: " + user.id)
2036
+ }
2037
+
2038
+ protected_route "/dashboard" { redirect: "/login" }
2039
+ protected_route "/dashboard/*" { redirect: "/login" }
2040
+ protected_route "/settings" { redirect: "/login" }
2041
+ }
2042
+
2043
+ server {
2044
+ fn get_message() {
2045
+ { text: "Hello from ${name}!", timestamp: Date.new().toLocaleTimeString() }
2046
+ }
2047
+
2048
+ route GET "/api/message" => get_message
2049
+ }
2050
+
2051
+ browser {
2052
+ state message = ""
2053
+ state timestamp = ""
2054
+ state refreshing = false
2055
+
2056
+ effect {
2057
+ result = server.get_message()
2058
+ message = result.text
2059
+ timestamp = result.timestamp
2060
+ }
2061
+
2062
+ fn handle_refresh() {
2063
+ refreshing = true
2064
+ result = server.get_message()
2065
+ message = result.text
2066
+ timestamp = result.timestamp
2067
+ refreshing = false
2068
+ }
2069
+
2070
+ // \u2500\u2500\u2500 Navigation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2071
+ component NavBar {
2072
+ <nav class="border-b border-gray-200 bg-white shadow-sm sticky top-0 z-10">
2073
+ <div class="max-w-5xl mx-auto px-6 py-4 flex items-center justify-between">
2074
+ <Link href="/" class="flex items-center gap-2 no-underline">
2075
+ <div class="w-8 h-8 bg-gradient-to-br from-emerald-500 to-teal-600 rounded-lg flex items-center justify-center">
2076
+ <span class="text-white font-bold text-sm">"T"</span>
2077
+ </div>
2078
+ <span class="font-bold text-gray-900 text-lg">"${name}"</span>
2079
+ </Link>
2080
+ <div class="flex items-center gap-4">
2081
+ <Link href="/" exactActiveClass="text-emerald-600 font-semibold" class="text-sm font-medium text-gray-500 hover:text-gray-900 no-underline">"Home"</Link>
2082
+ if $isAuthenticated {
2083
+ <Link href="/dashboard" activeClass="text-emerald-600 font-semibold" class="text-sm font-medium text-gray-500 hover:text-gray-900 no-underline">"Dashboard"</Link>
2084
+ <div class="flex items-center gap-3 ml-2 pl-4 border-l border-gray-200">
2085
+ <span class="text-sm text-gray-500">
2086
+ if $currentUser != null {
2087
+ {$currentUser.email}
2088
+ }
2089
+ </span>
2090
+ <button
2091
+ on:click={fn() { logout() }}
2092
+ class="text-sm font-medium text-red-600 hover:text-red-700 bg-red-50 hover:bg-red-100 px-3 py-1.5 rounded-lg transition-colors"
2093
+ >
2094
+ "Sign Out"
2095
+ </button>
2096
+ </div>
2097
+ } else {
2098
+ <Link href="/login" class="text-sm font-medium text-gray-500 hover:text-gray-900 no-underline">"Login"</Link>
2099
+ <Link href="/signup" class="text-sm font-medium text-white bg-emerald-600 hover:bg-emerald-700 px-4 py-1.5 rounded-lg no-underline transition-colors">"Sign Up"</Link>
2100
+ }
2101
+ </div>
2102
+ </div>
2103
+ </nav>
2104
+ }
2105
+
2106
+ // \u2500\u2500\u2500 Pages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2107
+ component FeatureCard(icon, title, description) {
2108
+ <div class="group relative bg-white rounded-2xl p-6 shadow-sm border border-gray-100 hover:shadow-lg hover:border-emerald-100 transition-all duration-300">
2109
+ <div class="w-10 h-10 bg-emerald-50 rounded-xl flex items-center justify-center text-lg mb-4 group-hover:bg-emerald-100 transition-colors">
2110
+ "{icon}"
2111
+ </div>
2112
+ <h3 class="font-semibold text-gray-900 mb-1">"{title}"</h3>
2113
+ <p class="text-sm text-gray-500 leading-relaxed">"{description}"</p>
2114
+ </div>
2115
+ }
2116
+
2117
+ component HomePage {
2118
+ <main class="max-w-5xl mx-auto px-6">
2119
+ <div class="py-20 text-center">
2120
+ <div class="inline-flex items-center gap-2 bg-emerald-50 text-emerald-700 text-sm font-medium px-4 py-1.5 rounded-full mb-6">
2121
+ <span class="w-1.5 h-1.5 bg-emerald-500 rounded-full animate-pulse"></span>
2122
+ "Secure by Default"
2123
+ </div>
2124
+ <h1 class="text-5xl font-bold text-gray-900 tracking-tight mb-4">"Welcome to " <span class="bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent">"${name}"</span></h1>
2125
+ <p class="text-xl text-gray-500 max-w-2xl mx-auto mb-10">"A full-stack app with authentication. Edit " <code class="text-sm bg-gray-100 text-emerald-600 px-2 py-1 rounded-md font-mono">"src/app.tova"</code> " to get started."</p>
2126
+
2127
+ if $isAuthenticated {
2128
+ <Link href="/dashboard" class="inline-block bg-emerald-600 hover:bg-emerald-700 text-white font-medium px-6 py-3 rounded-xl no-underline transition-colors">
2129
+ "Go to Dashboard"
2130
+ </Link>
2131
+ } else {
2132
+ <div class="flex items-center justify-center gap-4">
2133
+ <Link href="/signup" class="inline-block bg-emerald-600 hover:bg-emerald-700 text-white font-medium px-6 py-3 rounded-xl no-underline transition-colors">"Get Started"</Link>
2134
+ <Link href="/login" class="inline-block bg-white border border-gray-200 hover:border-gray-300 text-gray-700 font-medium px-6 py-3 rounded-xl no-underline transition-colors">"Sign In"</Link>
2135
+ </div>
2136
+ }
2137
+
2138
+ if timestamp != "" {
2139
+ <p class="text-xs text-gray-400 mt-6">"Server time: " "{timestamp}"</p>
2140
+ }
2141
+ </div>
2142
+
2143
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-5 pb-20">
2144
+ <FeatureCard
2145
+ icon="\u2699"
2146
+ title="Full-Stack"
2147
+ description="Server and client in one file. Shared types, RPC calls, and reactive UI."
2148
+ />
2149
+ <FeatureCard
2150
+ icon="\uD83D\uDD12"
2151
+ title="Auth Built-in"
2152
+ description="Email signup, login, password reset, and JWT sessions \u2014 secure by default."
2153
+ />
2154
+ <FeatureCard
2155
+ icon="\uD83D\uDEE1"
2156
+ title="Security Hardened"
2157
+ description="CORS, CSRF, CSP, rate limiting, brute-force lockout, and HttpOnly cookies."
2158
+ />
2159
+ </div>
2160
+ </main>
2161
+ }
2162
+
2163
+ // \u2500\u2500\u2500 Auth Pages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2164
+ component LoginPage {
2165
+ <main class="max-w-md mx-auto px-6 py-16">
2166
+ <h2 class="text-2xl font-bold text-gray-900 mb-6 text-center">"Sign In"</h2>
2167
+ <div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-8">
2168
+ <LoginForm />
2169
+ <div class="mt-6 text-center">
2170
+ <Link href="/forgot-password" class="text-sm text-emerald-600 hover:text-emerald-700 no-underline">"Forgot your password?"</Link>
2171
+ </div>
2172
+ <div class="mt-4 text-center">
2173
+ <span class="text-sm text-gray-500">"Don't have an account? "</span>
2174
+ <Link href="/signup" class="text-sm text-emerald-600 hover:text-emerald-700 font-medium no-underline">"Sign Up"</Link>
2175
+ </div>
2176
+ </div>
2177
+ </main>
2178
+ }
2179
+
2180
+ component SignupPage {
2181
+ <main class="max-w-md mx-auto px-6 py-16">
2182
+ <h2 class="text-2xl font-bold text-gray-900 mb-6 text-center">"Create Account"</h2>
2183
+ <div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-8">
2184
+ <SignupForm />
2185
+ <div class="mt-4 text-center">
2186
+ <span class="text-sm text-gray-500">"Already have an account? "</span>
2187
+ <Link href="/login" class="text-sm text-emerald-600 hover:text-emerald-700 font-medium no-underline">"Sign In"</Link>
2188
+ </div>
2189
+ </div>
2190
+ </main>
2191
+ }
2192
+
2193
+ component ForgotPasswordPage {
2194
+ <main class="max-w-md mx-auto px-6 py-16">
2195
+ <h2 class="text-2xl font-bold text-gray-900 mb-6 text-center">"Reset Password"</h2>
2196
+ <div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-8">
2197
+ <ForgotPasswordForm />
2198
+ <div class="mt-4 text-center">
2199
+ <Link href="/login" class="text-sm text-emerald-600 hover:text-emerald-700 no-underline">"Back to login"</Link>
2200
+ </div>
2201
+ </div>
2202
+ </main>
2203
+ }
2204
+
2205
+ // \u2500\u2500\u2500 Protected Pages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2206
+ component DashboardPage {
2207
+ <AuthGuard redirect="/login">
2208
+ <main class="max-w-5xl mx-auto px-6 py-8">
2209
+ <div class="mb-8">
2210
+ <h2 class="text-2xl font-bold text-gray-900">"Dashboard"</h2>
2211
+ <p class="text-gray-500">
2212
+ "Welcome back"
2213
+ if $currentUser != null {
2214
+ ", " "{$currentUser.email}"
2215
+ }
2216
+ </p>
2217
+ </div>
2218
+
2219
+ <div class="bg-white rounded-xl border border-gray-200 p-8">
2220
+ <div class="text-center">
2221
+ <div class="inline-flex items-center gap-3 bg-emerald-50 border border-emerald-200 rounded-2xl p-3">
2222
+ <div class="bg-gradient-to-r from-emerald-500 to-teal-500 text-white px-5 py-2.5 rounded-xl font-medium">
2223
+ "{message}"
2224
+ </div>
2225
+ <button
2226
+ on:click={handle_refresh}
2227
+ class="px-4 py-2.5 text-gray-500 hover:text-emerald-600 hover:bg-emerald-50 rounded-xl transition-all font-medium text-sm"
2228
+ >
2229
+ if refreshing { "..." } else { "Refresh" }
2230
+ </button>
2231
+ </div>
2232
+ if timestamp != "" {
2233
+ <p class="text-xs text-gray-400 mt-3">"Server time: " "{timestamp}"</p>
2234
+ }
2235
+ </div>
2236
+ </div>
2237
+ </main>
2238
+ </AuthGuard>
2239
+ }
2240
+
2241
+ component SettingsPage {
2242
+ <AuthGuard redirect="/login">
2243
+ <main class="max-w-2xl mx-auto px-6 py-8">
2244
+ <h2 class="text-2xl font-bold text-gray-900 mb-6">"Settings"</h2>
2245
+ <div class="bg-white rounded-xl border border-gray-200 p-6">
2246
+ <h3 class="font-semibold text-gray-900 mb-4">"Account"</h3>
2247
+ if $currentUser != null {
2248
+ <div class="space-y-3">
2249
+ <div>
2250
+ <span class="text-sm text-gray-500">"Email: "</span>
2251
+ <span class="text-sm font-medium text-gray-900">{$currentUser.email}</span>
2252
+ </div>
2253
+ <div>
2254
+ <span class="text-sm text-gray-500">"Role: "</span>
2255
+ <span class="text-sm font-medium text-gray-900">{$currentUser.role}</span>
2256
+ </div>
2257
+ </div>
2258
+ }
2259
+ </div>
2260
+ </main>
2261
+ </AuthGuard>
2262
+ }
2263
+
2264
+ component NotFoundPage {
2265
+ <div class="max-w-5xl mx-auto px-6 py-16 text-center">
2266
+ <h1 class="text-6xl font-bold text-gray-200 mb-4">"404"</h1>
2267
+ <p class="text-lg text-gray-500 mb-6">"Page not found"</p>
2268
+ <Link href="/" class="text-emerald-600 hover:text-emerald-700 font-medium no-underline">"Go home"</Link>
2269
+ </div>
2270
+ }
2271
+
2272
+ // \u2500\u2500\u2500 Router \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2273
+ createRouter({
2274
+ routes: {
2275
+ "/": HomePage,
2276
+ "/login": LoginPage,
2277
+ "/signup": SignupPage,
2278
+ "/forgot-password": ForgotPasswordPage,
2279
+ "/dashboard": DashboardPage,
2280
+ "/settings": SettingsPage,
2281
+ "404": NotFoundPage,
2282
+ },
2283
+ scroll: "auto",
2284
+ })
2285
+
2286
+ component App {
2287
+ <div class="min-h-screen bg-gradient-to-br from-slate-50 via-white to-emerald-50">
2288
+ <NavBar />
2289
+ <Router />
2290
+ <div class="border-t border-gray-100 py-8 text-center">
2291
+ <p class="text-sm text-gray-400">"Built with " <a href="https://github.com/tova-lang/tova-lang" class="text-emerald-500 hover:text-emerald-600 transition-colors">"Tova"</a></p>
2292
+ </div>
2293
+ </div>
2294
+ }
2295
+ }
2296
+ `;
2297
+ }
2298
+
2299
+ // ─── Auth template content (SPA + auth) ──────────────────────────
2300
+ function spaAuthContent(name) {
2301
+ return `// ${name} — Built with Tova
2302
+ // Single-page app with authentication, nested routes, and dynamic params
2303
+
2304
+ shared {
2305
+ type User {
2306
+ id: String
2307
+ email: String
2308
+ role: String
2309
+ }
2310
+ }
2311
+
2312
+ security {
2313
+ cors {
2314
+ origins: ["http://localhost:3000"]
2315
+ methods: ["GET", "POST", "PUT", "DELETE"]
2316
+ credentials: true
2317
+ }
2318
+
2319
+ csrf {
2320
+ enabled: true
2321
+ }
2322
+
2323
+ rate_limit {
2324
+ window: 60
2325
+ max: 100
2326
+ }
2327
+
2328
+ csp {
2329
+ default_src: ["self"]
2330
+ script_src: ["self", "https://cdn.tailwindcss.com"]
2331
+ style_src: ["self", "unsafe-inline"]
2332
+ img_src: ["self", "data:", "https:"]
2333
+ connect_src: ["self"]
2334
+ }
2335
+ }
2336
+
2337
+ auth {
2338
+ secret: env("AUTH_SECRET")
2339
+ token_expires: 900
2340
+ refresh_expires: 604800
2341
+ storage: "cookie"
2342
+
2343
+ provider email {
2344
+ confirm_email: true
2345
+ password_min: 8
2346
+ max_attempts: 5
2347
+ lockout_duration: 900
2348
+ }
2349
+
2350
+ on signup fn(user) {
2351
+ print("New user signed up: " + user.email)
2352
+ }
2353
+
2354
+ on login fn(user) {
2355
+ print("User logged in: " + user.email)
2356
+ }
2357
+
2358
+ on logout fn(user) {
2359
+ print("User logged out: " + user.id)
2360
+ }
2361
+
2362
+ protected_route "/dashboard" { redirect: "/login" }
2363
+ protected_route "/profile/*" { redirect: "/login" }
2364
+ }
2365
+
2366
+ server {
2367
+ // Auth endpoints (signup, login, logout, etc.) are generated automatically
2368
+ fn health_check() {
2369
+ { status: "ok" }
2370
+ }
2371
+
2372
+ route GET "/api/health" => health_check
2373
+ }
2374
+
2375
+ browser {
2376
+ // \u2500\u2500\u2500 Navigation bar with auth-aware links \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2377
+ component NavBar {
2378
+ <nav class="bg-white border-b border-gray-100 sticky top-0 z-10">
2379
+ <div class="max-w-5xl mx-auto px-6 py-4 flex items-center justify-between">
2380
+ <Link href="/" class="flex items-center gap-2 no-underline">
2381
+ <div class="w-8 h-8 bg-gradient-to-br from-emerald-500 to-teal-600 rounded-lg flex items-center justify-center">
2382
+ <span class="text-white font-bold text-sm">"T"</span>
2383
+ </div>
2384
+ <span class="font-bold text-gray-900 text-lg">"${name}"</span>
2385
+ </Link>
2386
+ <div class="flex items-center gap-4">
2387
+ <Link href="/" exactActiveClass="text-emerald-600 font-semibold" class="text-sm font-medium text-gray-500 hover:text-gray-900 no-underline">"Home"</Link>
2388
+ if $isAuthenticated {
2389
+ <Link href="/dashboard" activeClass="text-emerald-600 font-semibold" class="text-sm font-medium text-gray-500 hover:text-gray-900 no-underline">"Dashboard"</Link>
2390
+ <Link href="/profile" activeClass="text-emerald-600 font-semibold" class="text-sm font-medium text-gray-500 hover:text-gray-900 no-underline">"Profile"</Link>
2391
+ <div class="flex items-center gap-3 ml-2 pl-4 border-l border-gray-200">
2392
+ <span class="text-sm text-gray-500">
2393
+ if $currentUser != null {
2394
+ {$currentUser.email}
2395
+ }
2396
+ </span>
2397
+ <button
2398
+ on:click={fn() { logout() }}
2399
+ class="text-sm font-medium text-red-600 hover:text-red-700 bg-red-50 hover:bg-red-100 px-3 py-1.5 rounded-lg transition-colors"
2400
+ >
2401
+ "Sign Out"
2402
+ </button>
2403
+ </div>
2404
+ } else {
2405
+ <Link href="/login" class="text-sm font-medium text-gray-500 hover:text-gray-900 no-underline">"Login"</Link>
2406
+ <Link href="/signup" class="text-sm font-medium text-white bg-emerald-600 hover:bg-emerald-700 px-4 py-1.5 rounded-lg no-underline transition-colors">"Sign Up"</Link>
2407
+ }
2408
+ </div>
2409
+ </div>
2410
+ </nav>
2411
+ }
2412
+
2413
+ // \u2500\u2500\u2500 Home page \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2414
+ component HomePage {
2415
+ <div class="max-w-5xl mx-auto px-6 py-16 text-center">
2416
+ <div class="inline-flex items-center gap-2 bg-emerald-50 text-emerald-700 text-sm font-medium px-4 py-1.5 rounded-full mb-6">
2417
+ <span class="w-1.5 h-1.5 bg-emerald-500 rounded-full animate-pulse"></span>
2418
+ "Secure SPA"
2419
+ </div>
2420
+ <h1 class="text-4xl font-bold text-gray-900 mb-4">"Welcome to " <span class="text-emerald-600">"${name}"</span></h1>
2421
+ <p class="text-lg text-gray-500 mb-8">"A single-page app with authentication, nested routes, and dynamic params."</p>
2422
+ if $isAuthenticated {
2423
+ <Link href="/dashboard" class="inline-block bg-emerald-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-emerald-700 transition-colors no-underline">"Go to Dashboard"</Link>
2424
+ } else {
2425
+ <div class="flex items-center justify-center gap-4">
2426
+ <Link href="/signup" class="inline-block bg-emerald-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-emerald-700 transition-colors no-underline">"Get Started"</Link>
2427
+ <Link href="/login" class="inline-block bg-white text-gray-700 border border-gray-200 px-6 py-3 rounded-lg font-medium hover:bg-gray-50 transition-colors no-underline">"Sign In"</Link>
2428
+ </div>
2429
+ }
2430
+ </div>
2431
+ }
2432
+
2433
+ // \u2500\u2500\u2500 Auth Pages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2434
+ component LoginPage {
2435
+ <main class="max-w-md mx-auto px-6 py-16">
2436
+ <h2 class="text-2xl font-bold text-gray-900 mb-6 text-center">"Sign In"</h2>
2437
+ <div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-8">
2438
+ <LoginForm />
2439
+ <div class="mt-6 text-center">
2440
+ <Link href="/forgot-password" class="text-sm text-emerald-600 hover:text-emerald-700 no-underline">"Forgot your password?"</Link>
2441
+ </div>
2442
+ <div class="mt-4 text-center">
2443
+ <span class="text-sm text-gray-500">"Don't have an account? "</span>
2444
+ <Link href="/signup" class="text-sm text-emerald-600 hover:text-emerald-700 font-medium no-underline">"Sign Up"</Link>
2445
+ </div>
2446
+ </div>
2447
+ </main>
2448
+ }
2449
+
2450
+ component SignupPage {
2451
+ <main class="max-w-md mx-auto px-6 py-16">
2452
+ <h2 class="text-2xl font-bold text-gray-900 mb-6 text-center">"Create Account"</h2>
2453
+ <div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-8">
2454
+ <SignupForm />
2455
+ <div class="mt-4 text-center">
2456
+ <span class="text-sm text-gray-500">"Already have an account? "</span>
2457
+ <Link href="/login" class="text-sm text-emerald-600 hover:text-emerald-700 font-medium no-underline">"Sign In"</Link>
2458
+ </div>
2459
+ </div>
2460
+ </main>
2461
+ }
2462
+
2463
+ component ForgotPasswordPage {
2464
+ <main class="max-w-md mx-auto px-6 py-16">
2465
+ <h2 class="text-2xl font-bold text-gray-900 mb-6 text-center">"Reset Password"</h2>
2466
+ <div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-8">
2467
+ <ForgotPasswordForm />
2468
+ <div class="mt-4 text-center">
2469
+ <Link href="/login" class="text-sm text-emerald-600 hover:text-emerald-700 no-underline">"Back to login"</Link>
2470
+ </div>
2471
+ </div>
2472
+ </main>
2473
+ }
2474
+
2475
+ // \u2500\u2500\u2500 Dashboard (protected) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2476
+ component DashboardPage {
2477
+ <AuthGuard redirect="/login">
2478
+ <main class="max-w-5xl mx-auto px-6 py-8">
2479
+ <h2 class="text-2xl font-bold text-gray-900 mb-2">"Dashboard"</h2>
2480
+ <p class="text-gray-500 mb-8">
2481
+ "Welcome back"
2482
+ if $currentUser != null {
2483
+ ", " "{$currentUser.email}"
2484
+ }
2485
+ </p>
2486
+ <div class="bg-white rounded-xl border border-gray-200 p-6">
2487
+ <p class="text-gray-600">"This is a protected page. Only authenticated users can see this."</p>
2488
+ </div>
2489
+ </main>
2490
+ </AuthGuard>
2491
+ }
2492
+
2493
+ // \u2500\u2500\u2500 Profile layout with nested routes + Outlet \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2494
+ component ProfileLayout {
2495
+ <AuthGuard redirect="/login">
2496
+ <div class="max-w-5xl mx-auto px-6 py-8">
2497
+ <h2 class="text-2xl font-bold text-gray-900 mb-6">"Profile"</h2>
2498
+ <div class="flex gap-8">
2499
+ <aside class="w-48 flex-shrink-0">
2500
+ <div class="flex flex-col gap-1">
2501
+ <Link href="/profile/account" activeClass="bg-emerald-50 text-emerald-700" class="block px-3 py-2 rounded-lg text-sm font-medium text-gray-600 hover:bg-gray-50 no-underline transition-colors">"Account"</Link>
2502
+ <Link href="/profile/security" activeClass="bg-emerald-50 text-emerald-700" class="block px-3 py-2 rounded-lg text-sm font-medium text-gray-600 hover:bg-gray-50 no-underline transition-colors">"Security"</Link>
2503
+ </div>
2504
+ </aside>
2505
+ <div class="flex-1 min-w-0">
2506
+ <Outlet />
2507
+ </div>
2508
+ </div>
2509
+ </div>
2510
+ </AuthGuard>
2511
+ }
2512
+
2513
+ component AccountSettings {
2514
+ <div class="bg-white rounded-xl border border-gray-200 p-6">
2515
+ <h3 class="text-lg font-semibold text-gray-900 mb-4">"Account Settings"</h3>
2516
+ if $currentUser != null {
2517
+ <div class="space-y-4">
2518
+ <div>
2519
+ <label class="block text-sm font-medium text-gray-700 mb-1">"Email"</label>
2520
+ <div class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm text-gray-900">{$currentUser.email}</div>
2521
+ </div>
2522
+ <div>
2523
+ <label class="block text-sm font-medium text-gray-700 mb-1">"Role"</label>
2524
+ <div class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm text-gray-900">{$currentUser.role}</div>
2525
+ </div>
2526
+ </div>
2527
+ }
2528
+ </div>
2529
+ }
2530
+
2531
+ component SecuritySettings {
2532
+ <div class="bg-white rounded-xl border border-gray-200 p-6">
2533
+ <h3 class="text-lg font-semibold text-gray-900 mb-4">"Security Settings"</h3>
2534
+ <p class="text-gray-600 mb-4">"Manage your password and security preferences."</p>
2535
+ <div class="pt-4 border-t border-gray-100">
2536
+ <Link href="/forgot-password" class="text-sm text-emerald-600 hover:text-emerald-700 font-medium no-underline">"Change Password"</Link>
2537
+ </div>
2538
+ </div>
2539
+ }
2540
+
2541
+ // \u2500\u2500\u2500 404 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2542
+ component NotFoundPage {
2543
+ <div class="max-w-5xl mx-auto px-6 py-16 text-center">
2544
+ <h1 class="text-6xl font-bold text-gray-200 mb-4">"404"</h1>
2545
+ <p class="text-lg text-gray-500 mb-6">"Page not found"</p>
2546
+ <Link href="/" class="text-emerald-600 hover:text-emerald-700 font-medium no-underline">"Go home"</Link>
2547
+ </div>
2548
+ }
2549
+
2550
+ // \u2500\u2500\u2500 Router \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2551
+ createRouter({
2552
+ routes: {
2553
+ "/": HomePage,
2554
+ "/login": LoginPage,
2555
+ "/signup": SignupPage,
2556
+ "/forgot-password": ForgotPasswordPage,
2557
+ "/dashboard": { component: DashboardPage, meta: { title: "Dashboard" } },
2558
+ "/profile": {
2559
+ component: ProfileLayout,
2560
+ children: {
2561
+ "/account": { component: AccountSettings, meta: { title: "Account" } },
2562
+ "/security": { component: SecuritySettings, meta: { title: "Security" } },
2563
+ },
2564
+ },
2565
+ "404": NotFoundPage,
2566
+ },
2567
+ scroll: "auto",
2568
+ })
2569
+
2570
+ // \u2500\u2500\u2500 Update document title from route meta \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2571
+ afterNavigate(fn(current) {
2572
+ if current.meta != undefined {
2573
+ if current.meta.title != undefined {
2574
+ document.title = "{current.meta.title} | ${name}"
2575
+ }
2576
+ }
2577
+ })
2578
+
2579
+ component App {
2580
+ <div class="min-h-screen bg-gray-50">
2581
+ <NavBar />
2582
+ <Router />
2583
+ </div>
2584
+ }
2585
+ }
2586
+ `;
2587
+ }
2588
+
1975
2589
  const PROJECT_TEMPLATES = {
1976
2590
  fullstack: {
1977
2591
  label: 'Full-stack app',
@@ -2136,6 +2750,9 @@ browser {
2136
2750
  }
2137
2751
  `,
2138
2752
  nextSteps: name => ` cd ${name}\n tova dev`,
2753
+ hasAuthOption: true,
2754
+ authContent: fullstackAuthContent,
2755
+ authNextSteps: name => ` cd ${name}\n tova dev\n\n ${color.dim('Auth is ready! Sign up at')} ${color.cyan('http://localhost:3000/signup')}`,
2139
2756
  },
2140
2757
  spa: {
2141
2758
  label: 'Single-page app',
@@ -2341,6 +2958,9 @@ browser {
2341
2958
  }
2342
2959
  `,
2343
2960
  nextSteps: name => ` cd ${name}\n tova dev`,
2961
+ hasAuthOption: true,
2962
+ authContent: spaAuthContent,
2963
+ authNextSteps: name => ` cd ${name}\n tova dev\n\n ${color.dim('Auth is ready! Sign up at')} ${color.cyan('http://localhost:3000/signup')}`,
2344
2964
  },
2345
2965
  site: {
2346
2966
  label: 'Static site',
@@ -2541,7 +3161,7 @@ async function newProject(rawArgs) {
2541
3161
 
2542
3162
  if (!name) {
2543
3163
  console.error(color.red('Error: No project name specified'));
2544
- console.error('Usage: tova new <project-name> [--template fullstack|spa|site|api|script|library|blank]');
3164
+ console.error('Usage: tova new <project-name> [--template fullstack|spa|site|api|script|library|blank] [--auth]');
2545
3165
  process.exit(1);
2546
3166
  }
2547
3167
 
@@ -2589,7 +3209,30 @@ async function newProject(rawArgs) {
2589
3209
  }
2590
3210
 
2591
3211
  const template = PROJECT_TEMPLATES[templateName];
2592
- console.log(`\n ${color.bold('Creating new Tova project:')} ${color.cyan(name)} ${color.dim(`(${template.label})`)}\n`);
3212
+ const authFlag = rawArgs.includes('--auth');
3213
+
3214
+ // Ask about auth if template supports it
3215
+ // Only prompt interactively when template was selected via picker (not --template flag)
3216
+ let withAuth = false;
3217
+ if (template.hasAuthOption) {
3218
+ if (authFlag) {
3219
+ withAuth = true;
3220
+ } else if (!templateFlag) {
3221
+ // Interactive mode — template was selected via picker, so ask about auth
3222
+ const { createInterface: createRl } = await import('readline');
3223
+ const rl2 = createRl({ input: process.stdin, output: process.stdout });
3224
+ const authAnswer = await new Promise(resolve => {
3225
+ rl2.question(` Include authentication? ${color.dim('[y/N]')}: `, ans => {
3226
+ rl2.close();
3227
+ resolve(ans.trim().toLowerCase());
3228
+ });
3229
+ });
3230
+ withAuth = authAnswer === 'y' || authAnswer === 'yes';
3231
+ }
3232
+ }
3233
+
3234
+ const templateLabel = withAuth ? `${template.label} + Auth` : template.label;
3235
+ console.log(`\n ${color.bold('Creating new Tova project:')} ${color.cyan(name)} ${color.dim(`(${templateLabel})`)}\n`);
2593
3236
 
2594
3237
  // Create directories
2595
3238
  mkdirSync(projectDir, { recursive: true });
@@ -2644,19 +3287,22 @@ async function newProject(rawArgs) {
2644
3287
  createdFiles.push('tova.toml');
2645
3288
 
2646
3289
  // .gitignore
2647
- writeFileSync(join(projectDir, '.gitignore'), `node_modules/
3290
+ let gitignoreContent = `node_modules/
2648
3291
  .tova-out/
2649
3292
  package.json
2650
3293
  bun.lock
2651
3294
  *.db
2652
3295
  *.db-shm
2653
3296
  *.db-wal
2654
- `);
3297
+ `;
3298
+ if (withAuth) gitignoreContent += `.env\n`;
3299
+ writeFileSync(join(projectDir, '.gitignore'), gitignoreContent);
2655
3300
  createdFiles.push('.gitignore');
2656
3301
 
2657
3302
  // Template source file
2658
- if (template.file && template.content) {
2659
- writeFileSync(join(projectDir, template.file), template.content(projectName));
3303
+ const contentFn = withAuth && template.authContent ? template.authContent : template.content;
3304
+ if (template.file && contentFn) {
3305
+ writeFileSync(join(projectDir, template.file), contentFn(projectName));
2660
3306
  createdFiles.push(template.file);
2661
3307
  }
2662
3308
 
@@ -2670,6 +3316,16 @@ bun.lock
2670
3316
  }
2671
3317
  }
2672
3318
 
3319
+ // Auth files (.env + .env.example)
3320
+ if (withAuth) {
3321
+ const { randomBytes } = await import('crypto');
3322
+ const authSecret = randomBytes(32).toString('hex');
3323
+ writeFileSync(join(projectDir, '.env'), `# Auto-generated for development \u2014 do not commit this file\nAUTH_SECRET=${authSecret}\n`);
3324
+ writeFileSync(join(projectDir, '.env.example'), `# Auth secret \u2014 used to sign JWT tokens\n# For production, generate a new one:\n# node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"\nAUTH_SECRET=change-me-to-a-random-secret\n`);
3325
+ createdFiles.push('.env');
3326
+ createdFiles.push('.env.example');
3327
+ }
3328
+
2673
3329
  // README
2674
3330
  let readmeContent = `# ${projectName}
2675
3331
 
@@ -2724,7 +3380,8 @@ tova add github.com/yourname/${projectName}
2724
3380
  } catch {}
2725
3381
 
2726
3382
  console.log(`\n ${color.green('Done!')} Next steps:\n`);
2727
- console.log(color.cyan(template.nextSteps(name)));
3383
+ const nextStepsFn = withAuth && template.authNextSteps ? template.authNextSteps : template.nextSteps;
3384
+ console.log(nextStepsFn(name));
2728
3385
  console.log('');
2729
3386
  }
2730
3387
 
@@ -4863,6 +5520,17 @@ function collectExports(ast, filename) {
4863
5520
  if (node.isPublic) publicExports.add(node.name);
4864
5521
  }
4865
5522
  if (node.type === 'ImplDeclaration') { /* impl doesn't export a name */ }
5523
+ if (node.type === 'ReExportDeclaration') {
5524
+ if (node.specifiers) {
5525
+ // Named re-exports: pub { a, b as c } from "module"
5526
+ for (const spec of node.specifiers) {
5527
+ publicExports.add(spec.exported);
5528
+ allNames.add(spec.exported);
5529
+ }
5530
+ }
5531
+ // Wildcard re-exports: pub * from "module" — can't enumerate statically,
5532
+ // but mark as having re-exports so import validation can allow through
5533
+ }
4866
5534
  }
4867
5535
 
4868
5536
  for (const node of ast.body) {