honertia 0.1.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.
- package/LICENSE +21 -0
- package/README.md +610 -0
- package/dist/auth.d.ts +10 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +11 -0
- package/dist/effect/action.d.ts +107 -0
- package/dist/effect/action.d.ts.map +1 -0
- package/dist/effect/action.js +150 -0
- package/dist/effect/auth.d.ts +94 -0
- package/dist/effect/auth.d.ts.map +1 -0
- package/dist/effect/auth.js +204 -0
- package/dist/effect/bridge.d.ts +40 -0
- package/dist/effect/bridge.d.ts.map +1 -0
- package/dist/effect/bridge.js +103 -0
- package/dist/effect/errors.d.ts +78 -0
- package/dist/effect/errors.d.ts.map +1 -0
- package/dist/effect/errors.js +37 -0
- package/dist/effect/handler.d.ts +25 -0
- package/dist/effect/handler.d.ts.map +1 -0
- package/dist/effect/handler.js +120 -0
- package/dist/effect/index.d.ts +16 -0
- package/dist/effect/index.d.ts.map +1 -0
- package/dist/effect/index.js +25 -0
- package/dist/effect/responses.d.ts +73 -0
- package/dist/effect/responses.d.ts.map +1 -0
- package/dist/effect/responses.js +104 -0
- package/dist/effect/routing.d.ts +90 -0
- package/dist/effect/routing.d.ts.map +1 -0
- package/dist/effect/routing.js +124 -0
- package/dist/effect/schema.d.ts +263 -0
- package/dist/effect/schema.d.ts.map +1 -0
- package/dist/effect/schema.js +586 -0
- package/dist/effect/services.d.ts +85 -0
- package/dist/effect/services.d.ts.map +1 -0
- package/dist/effect/services.js +24 -0
- package/dist/effect/validation.d.ts +38 -0
- package/dist/effect/validation.d.ts.map +1 -0
- package/dist/effect/validation.js +69 -0
- package/dist/helpers.d.ts +65 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +116 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/middleware.d.ts +14 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +113 -0
- package/dist/react.d.ts +17 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +4 -0
- package/dist/schema.d.ts +9 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +34 -0
- package/dist/setup.d.ts +113 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +96 -0
- package/dist/test-utils.d.ts +105 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/test-utils.js +210 -0
- package/dist/types.d.ts +37 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +11 -0
- package/package.json +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Patrick Ogilvie
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
# Honertia
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/honertia)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
An Inertia.js-style adapter for Hono with Effect.js integration. Build full-stack applications with type-safe server actions, Laravel-inspired validation, and seamless React rendering.
|
|
7
|
+
|
|
8
|
+
## Requirements
|
|
9
|
+
|
|
10
|
+
- **Runtime**: Node.js 18+ or Bun 1.0+
|
|
11
|
+
- **Peer Dependencies**:
|
|
12
|
+
- `hono` >= 4.0.0
|
|
13
|
+
- `better-auth` >= 1.0.0
|
|
14
|
+
- **Dependencies**:
|
|
15
|
+
- `effect` >= 3.12.0
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bun add honertia
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
// src/index.ts
|
|
27
|
+
import { Hono } from 'hono'
|
|
28
|
+
import {
|
|
29
|
+
honertia,
|
|
30
|
+
createTemplate,
|
|
31
|
+
loadUser,
|
|
32
|
+
shareAuthMiddleware,
|
|
33
|
+
effectBridge,
|
|
34
|
+
effectRoutes,
|
|
35
|
+
effectAuthRoutes,
|
|
36
|
+
RequireAuthLayer,
|
|
37
|
+
} from 'honertia'
|
|
38
|
+
|
|
39
|
+
const app = new Hono()
|
|
40
|
+
|
|
41
|
+
// Core middleware
|
|
42
|
+
app.use('*', honertia({
|
|
43
|
+
version: '1.0.0',
|
|
44
|
+
render: createTemplate({
|
|
45
|
+
title: 'My App',
|
|
46
|
+
scripts: ['http://localhost:5173/src/main.tsx'],
|
|
47
|
+
}),
|
|
48
|
+
}))
|
|
49
|
+
|
|
50
|
+
// Database & Auth setup (your own middleware)
|
|
51
|
+
app.use('*', async (c, next) => {
|
|
52
|
+
c.set('db', createDb(c.env.DATABASE_URL))
|
|
53
|
+
c.set('auth', createAuth({ /* config */ }))
|
|
54
|
+
await next()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// Auth middleware
|
|
58
|
+
app.use('*', loadUser())
|
|
59
|
+
app.use('*', shareAuthMiddleware())
|
|
60
|
+
|
|
61
|
+
// Effect bridge (sets up Effect runtime per request)
|
|
62
|
+
app.use('*', effectBridge())
|
|
63
|
+
|
|
64
|
+
// Register routes
|
|
65
|
+
effectAuthRoutes(app, {
|
|
66
|
+
loginComponent: 'Auth/Login',
|
|
67
|
+
registerComponent: 'Auth/Register',
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
effectRoutes(app)
|
|
71
|
+
.provide(RequireAuthLayer)
|
|
72
|
+
.group((route) => {
|
|
73
|
+
route.get('/', showDashboard)
|
|
74
|
+
route.get('/projects', listProjects)
|
|
75
|
+
route.post('/projects', createProject)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
export default app
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Using `setupHonertia` (Recommended)
|
|
82
|
+
|
|
83
|
+
For a cleaner setup, use `setupHonertia` which bundles all core middleware into a single call:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { Hono } from 'hono'
|
|
87
|
+
import { setupHonertia, createTemplate, effectRoutes } from 'honertia'
|
|
88
|
+
|
|
89
|
+
const app = new Hono()
|
|
90
|
+
|
|
91
|
+
app.use('*', setupHonertia({
|
|
92
|
+
honertia: {
|
|
93
|
+
version: '1.0.0',
|
|
94
|
+
render: createTemplate({
|
|
95
|
+
title: 'My App',
|
|
96
|
+
scripts: ['http://localhost:5173/src/main.tsx'],
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
auth: {
|
|
100
|
+
userKey: 'user',
|
|
101
|
+
sessionCookie: 'session',
|
|
102
|
+
},
|
|
103
|
+
effect: {
|
|
104
|
+
// Effect bridge configuration
|
|
105
|
+
},
|
|
106
|
+
}))
|
|
107
|
+
|
|
108
|
+
export default app
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
This is equivalent to manually registering:
|
|
112
|
+
- `honertia()` - Core Honertia middleware
|
|
113
|
+
- `loadUser()` - Loads authenticated user into context
|
|
114
|
+
- `shareAuthMiddleware()` - Shares auth state with pages
|
|
115
|
+
- `effectBridge()` - Sets up Effect runtime for each request
|
|
116
|
+
|
|
117
|
+
#### Adding Custom Middleware
|
|
118
|
+
|
|
119
|
+
You can inject additional middleware that runs after the core setup:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { cors } from 'hono/cors'
|
|
123
|
+
import { logger } from 'hono/logger'
|
|
124
|
+
|
|
125
|
+
app.use('*', setupHonertia({
|
|
126
|
+
honertia: {
|
|
127
|
+
version: '1.0.0',
|
|
128
|
+
render: createTemplate({ title: 'My App', scripts: [...] }),
|
|
129
|
+
},
|
|
130
|
+
middleware: [
|
|
131
|
+
cors(),
|
|
132
|
+
logger(),
|
|
133
|
+
myCustomMiddleware(),
|
|
134
|
+
],
|
|
135
|
+
}))
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The custom middleware runs in order after `effectBridge`, giving you access to all Honertia context variables.
|
|
139
|
+
|
|
140
|
+
#### Environment-Aware Templates
|
|
141
|
+
|
|
142
|
+
`createTemplate` can accept a function that receives the Hono context, enabling environment-specific configuration:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
const viteHmrHead = `
|
|
146
|
+
<script type="module">
|
|
147
|
+
import RefreshRuntime from 'http://localhost:5173/@react-refresh'
|
|
148
|
+
RefreshRuntime.injectIntoGlobalHook(window)
|
|
149
|
+
window.$RefreshReg$ = () => {}
|
|
150
|
+
window.$RefreshSig$ = () => (type) => type
|
|
151
|
+
window.__vite_plugin_react_preamble_installed__ = true
|
|
152
|
+
</script>
|
|
153
|
+
<script type="module" src="http://localhost:5173/@vite/client"></script>
|
|
154
|
+
`
|
|
155
|
+
|
|
156
|
+
app.use('*', setupHonertia({
|
|
157
|
+
honertia: {
|
|
158
|
+
version: '1.0.0',
|
|
159
|
+
render: createTemplate((ctx) => {
|
|
160
|
+
const isProd = ctx.env.ENVIRONMENT === 'production'
|
|
161
|
+
return {
|
|
162
|
+
title: 'My App',
|
|
163
|
+
scripts: isProd
|
|
164
|
+
? ['/assets/main.js']
|
|
165
|
+
: ['http://localhost:5173/src/main.tsx'],
|
|
166
|
+
head: isProd
|
|
167
|
+
? '<link rel="icon" href="/favicon.svg" />'
|
|
168
|
+
: `${viteHmrHead}<link rel="icon" href="/favicon.svg" />`,
|
|
169
|
+
}
|
|
170
|
+
}),
|
|
171
|
+
},
|
|
172
|
+
}))
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
This pattern allows you to:
|
|
176
|
+
- Use Vite HMR in development, built assets in production
|
|
177
|
+
- Access environment variables from `ctx.env`
|
|
178
|
+
- Dynamically configure any template option based on request context
|
|
179
|
+
|
|
180
|
+
## Core Concepts
|
|
181
|
+
|
|
182
|
+
### Effect-Based Handlers
|
|
183
|
+
|
|
184
|
+
Route handlers are Effect computations that return `Response | Redirect`:
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import { Effect } from 'effect'
|
|
188
|
+
import {
|
|
189
|
+
DatabaseService,
|
|
190
|
+
AuthUserService,
|
|
191
|
+
render,
|
|
192
|
+
redirect,
|
|
193
|
+
} from 'honertia'
|
|
194
|
+
|
|
195
|
+
// Simple page render
|
|
196
|
+
export const showDashboard = Effect.gen(function* () {
|
|
197
|
+
const db = yield* DatabaseService
|
|
198
|
+
const user = yield* AuthUserService
|
|
199
|
+
|
|
200
|
+
const projects = yield* Effect.tryPromise(() =>
|
|
201
|
+
db.query.projects.findMany({
|
|
202
|
+
where: eq(schema.projects.userId, user.user.id),
|
|
203
|
+
limit: 5,
|
|
204
|
+
})
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return yield* render('Dashboard/Index', { projects })
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
// Form submission with redirect
|
|
211
|
+
export const createProject = Effect.gen(function* () {
|
|
212
|
+
const db = yield* DatabaseService
|
|
213
|
+
const user = yield* AuthUserService
|
|
214
|
+
const input = yield* validateRequest(CreateProjectSchema)
|
|
215
|
+
|
|
216
|
+
yield* Effect.tryPromise(() =>
|
|
217
|
+
db.insert(schema.projects).values({
|
|
218
|
+
...input,
|
|
219
|
+
userId: user.user.id,
|
|
220
|
+
})
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
return yield* redirect('/projects')
|
|
224
|
+
})
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Services
|
|
228
|
+
|
|
229
|
+
Honertia provides these services via Effect's dependency injection:
|
|
230
|
+
|
|
231
|
+
| Service | Description |
|
|
232
|
+
|---------|-------------|
|
|
233
|
+
| `DatabaseService` | Database client (from `c.var.db`) |
|
|
234
|
+
| `AuthService` | Auth instance (from `c.var.auth`) |
|
|
235
|
+
| `AuthUserService` | Authenticated user session |
|
|
236
|
+
| `HonertiaService` | Page renderer |
|
|
237
|
+
| `RequestService` | Request context (params, query, body) |
|
|
238
|
+
| `ResponseFactoryService` | Response builders |
|
|
239
|
+
|
|
240
|
+
### Routing
|
|
241
|
+
|
|
242
|
+
Use `effectRoutes` for Laravel-style route definitions:
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
import {
|
|
246
|
+
effectRoutes,
|
|
247
|
+
RequireAuthLayer,
|
|
248
|
+
RequireGuestLayer,
|
|
249
|
+
} from 'honertia'
|
|
250
|
+
|
|
251
|
+
// Protected routes (require authentication)
|
|
252
|
+
effectRoutes(app)
|
|
253
|
+
.provide(RequireAuthLayer)
|
|
254
|
+
.prefix('/dashboard')
|
|
255
|
+
.group((route) => {
|
|
256
|
+
route.get('/', showDashboard)
|
|
257
|
+
route.get('/settings', showSettings)
|
|
258
|
+
route.post('/settings', updateSettings)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// Guest-only routes
|
|
262
|
+
effectRoutes(app)
|
|
263
|
+
.provide(RequireGuestLayer)
|
|
264
|
+
.group((route) => {
|
|
265
|
+
route.get('/login', showLogin)
|
|
266
|
+
route.get('/register', showRegister)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
// Public routes (no layer)
|
|
270
|
+
effectRoutes(app).group((route) => {
|
|
271
|
+
route.get('/about', showAbout)
|
|
272
|
+
route.get('/pricing', showPricing)
|
|
273
|
+
})
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Validation
|
|
277
|
+
|
|
278
|
+
Honertia uses Effect Schema with Laravel-inspired validators:
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
import { Effect, Schema as S } from 'effect'
|
|
282
|
+
import {
|
|
283
|
+
validateRequest,
|
|
284
|
+
requiredString,
|
|
285
|
+
nullableString,
|
|
286
|
+
email,
|
|
287
|
+
password,
|
|
288
|
+
redirect,
|
|
289
|
+
} from 'honertia'
|
|
290
|
+
|
|
291
|
+
// Define schema
|
|
292
|
+
const CreateProjectSchema = S.Struct({
|
|
293
|
+
name: requiredString.pipe(
|
|
294
|
+
S.minLength(3, { message: () => 'Name must be at least 3 characters' }),
|
|
295
|
+
S.maxLength(100)
|
|
296
|
+
),
|
|
297
|
+
description: nullableString,
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
// Use in handler
|
|
301
|
+
export const createProject = Effect.gen(function* () {
|
|
302
|
+
const input = yield* validateRequest(CreateProjectSchema, {
|
|
303
|
+
errorComponent: 'Projects/Create', // Re-render with errors on validation failure
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
// input is fully typed: { name: string, description: string | null }
|
|
307
|
+
yield* insertProject(input)
|
|
308
|
+
|
|
309
|
+
return yield* redirect('/projects')
|
|
310
|
+
})
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Available Validators
|
|
314
|
+
|
|
315
|
+
#### Strings
|
|
316
|
+
```typescript
|
|
317
|
+
import {
|
|
318
|
+
requiredString, // Trimmed, non-empty string
|
|
319
|
+
nullableString, // Converts empty to null
|
|
320
|
+
required, // Custom message: required('Name is required')
|
|
321
|
+
alpha, // Letters only
|
|
322
|
+
alphaDash, // Letters, numbers, dashes, underscores
|
|
323
|
+
alphaNum, // Letters and numbers only
|
|
324
|
+
email, // Validated email
|
|
325
|
+
url, // Validated URL
|
|
326
|
+
uuid, // UUID format
|
|
327
|
+
min, // min(5) - at least 5 chars
|
|
328
|
+
max, // max(100) - at most 100 chars
|
|
329
|
+
size, // size(10) - exactly 10 chars
|
|
330
|
+
} from 'honertia'
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
#### Numbers
|
|
334
|
+
```typescript
|
|
335
|
+
import {
|
|
336
|
+
coercedNumber, // Coerce string to number
|
|
337
|
+
positiveInt, // Positive integer
|
|
338
|
+
nonNegativeInt, // 0 or greater
|
|
339
|
+
between, // between(1, 100)
|
|
340
|
+
gt, gte, lt, lte, // Comparisons
|
|
341
|
+
} from 'honertia'
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
#### Booleans & Dates
|
|
345
|
+
```typescript
|
|
346
|
+
import {
|
|
347
|
+
coercedBoolean, // Coerce "true", "1", etc.
|
|
348
|
+
checkbox, // HTML checkbox (defaults to false)
|
|
349
|
+
accepted, // Must be truthy
|
|
350
|
+
coercedDate, // Coerce to Date
|
|
351
|
+
nullableDate, // Empty string -> null
|
|
352
|
+
after, // after(new Date())
|
|
353
|
+
before, // before('2025-01-01')
|
|
354
|
+
} from 'honertia'
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
#### Password
|
|
358
|
+
```typescript
|
|
359
|
+
import { password } from 'honertia'
|
|
360
|
+
|
|
361
|
+
const PasswordSchema = password({
|
|
362
|
+
min: 8,
|
|
363
|
+
letters: true,
|
|
364
|
+
mixedCase: true,
|
|
365
|
+
numbers: true,
|
|
366
|
+
symbols: true,
|
|
367
|
+
})
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## Response Helpers
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
import {
|
|
374
|
+
render,
|
|
375
|
+
renderWithErrors,
|
|
376
|
+
redirect,
|
|
377
|
+
json,
|
|
378
|
+
notFound,
|
|
379
|
+
forbidden,
|
|
380
|
+
} from 'honertia'
|
|
381
|
+
|
|
382
|
+
// Render a page
|
|
383
|
+
return yield* render('Projects/Show', { project })
|
|
384
|
+
|
|
385
|
+
// Render with validation errors
|
|
386
|
+
return yield* renderWithErrors('Projects/Create', {
|
|
387
|
+
name: 'Name is required',
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
// Redirect (303 by default for POST)
|
|
391
|
+
return yield* redirect('/projects')
|
|
392
|
+
return yield* redirect('/login', 302)
|
|
393
|
+
|
|
394
|
+
// JSON response
|
|
395
|
+
return yield* json({ success: true })
|
|
396
|
+
return yield* json({ error: 'Not found' }, 404)
|
|
397
|
+
|
|
398
|
+
// Error responses
|
|
399
|
+
return yield* notFound('Project')
|
|
400
|
+
return yield* forbidden('You cannot edit this project')
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
## Error Handling
|
|
404
|
+
|
|
405
|
+
Honertia provides typed errors:
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
import {
|
|
409
|
+
ValidationError,
|
|
410
|
+
UnauthorizedError,
|
|
411
|
+
NotFoundError,
|
|
412
|
+
ForbiddenError,
|
|
413
|
+
HttpError,
|
|
414
|
+
} from 'honertia'
|
|
415
|
+
|
|
416
|
+
// Validation errors automatically re-render with field errors
|
|
417
|
+
const input = yield* validateRequest(schema, {
|
|
418
|
+
errorComponent: 'Projects/Create',
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
// Manual error handling
|
|
422
|
+
const project = yield* Effect.tryPromise(() =>
|
|
423
|
+
db.query.projects.findFirst({ where: eq(id, projectId) })
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
if (!project) {
|
|
427
|
+
return yield* notFound('Project', projectId)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (project.userId !== user.user.id) {
|
|
431
|
+
return yield* forbidden('You cannot view this project')
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
## Authentication
|
|
436
|
+
|
|
437
|
+
### Layers
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
import { RequireAuthLayer, RequireGuestLayer } from 'honertia'
|
|
441
|
+
|
|
442
|
+
// Require authentication - fails with UnauthorizedError if no user
|
|
443
|
+
effectRoutes(app)
|
|
444
|
+
.provide(RequireAuthLayer)
|
|
445
|
+
.get('/dashboard', showDashboard)
|
|
446
|
+
|
|
447
|
+
// Require guest - fails if user IS logged in
|
|
448
|
+
effectRoutes(app)
|
|
449
|
+
.provide(RequireGuestLayer)
|
|
450
|
+
.get('/login', showLogin)
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### Helpers
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
import {
|
|
457
|
+
requireAuth,
|
|
458
|
+
requireGuest,
|
|
459
|
+
isAuthenticated,
|
|
460
|
+
currentUser,
|
|
461
|
+
} from 'honertia'
|
|
462
|
+
|
|
463
|
+
// In a handler
|
|
464
|
+
export const showProfile = Effect.gen(function* () {
|
|
465
|
+
const user = yield* requireAuth('/login') // Redirect to /login if not auth'd
|
|
466
|
+
return yield* render('Profile', { user: user.user })
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
// Check without failing
|
|
470
|
+
const authed = yield* isAuthenticated // boolean
|
|
471
|
+
const user = yield* currentUser // AuthUser | null
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### Built-in Auth Routes
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
import { effectAuthRoutes } from 'honertia'
|
|
478
|
+
|
|
479
|
+
effectAuthRoutes(app, {
|
|
480
|
+
loginPath: '/login', // GET: show login page
|
|
481
|
+
registerPath: '/register', // GET: show register page
|
|
482
|
+
logoutPath: '/logout', // POST: logout and redirect
|
|
483
|
+
apiPath: '/api/auth', // Better-auth API handler
|
|
484
|
+
logoutRedirect: '/login',
|
|
485
|
+
loginComponent: 'Auth/Login',
|
|
486
|
+
registerComponent: 'Auth/Register',
|
|
487
|
+
})
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
## Action Factories
|
|
491
|
+
|
|
492
|
+
For common patterns, use action factories:
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
import { effectAction, dbAction, authAction } from 'honertia'
|
|
496
|
+
|
|
497
|
+
// effectAction: validation + custom handler
|
|
498
|
+
export const updateSettings = effectAction(
|
|
499
|
+
SettingsSchema,
|
|
500
|
+
(input) => Effect.gen(function* () {
|
|
501
|
+
yield* saveSettings(input)
|
|
502
|
+
return yield* redirect('/settings')
|
|
503
|
+
}),
|
|
504
|
+
{ errorComponent: 'Settings/Edit' }
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
// dbAction: validation + db + auth
|
|
508
|
+
export const createProject = dbAction(
|
|
509
|
+
CreateProjectSchema,
|
|
510
|
+
(input, { db, user }) => Effect.gen(function* () {
|
|
511
|
+
yield* Effect.tryPromise(() =>
|
|
512
|
+
db.insert(projects).values({ ...input, userId: user.user.id })
|
|
513
|
+
)
|
|
514
|
+
return yield* redirect('/projects')
|
|
515
|
+
}),
|
|
516
|
+
{ errorComponent: 'Projects/Create' }
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
// authAction: just requires auth
|
|
520
|
+
export const showDashboard = authAction((user) =>
|
|
521
|
+
Effect.gen(function* () {
|
|
522
|
+
const data = yield* fetchDashboardData(user)
|
|
523
|
+
return yield* render('Dashboard', data)
|
|
524
|
+
})
|
|
525
|
+
)
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
## React Integration
|
|
529
|
+
|
|
530
|
+
### Page Component Type
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
import type { HonertiaPage } from 'honertia'
|
|
534
|
+
|
|
535
|
+
interface ProjectsProps {
|
|
536
|
+
projects: Project[]
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const ProjectsIndex: HonertiaPage<ProjectsProps> = ({ projects, errors }) => {
|
|
540
|
+
return (
|
|
541
|
+
<div>
|
|
542
|
+
{errors?.name && <span className="error">{errors.name}</span>}
|
|
543
|
+
{projects.map(p => <ProjectCard key={p.id} project={p} />)}
|
|
544
|
+
</div>
|
|
545
|
+
)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export default ProjectsIndex
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### Shared Props
|
|
552
|
+
|
|
553
|
+
All pages receive shared props set via middleware:
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
// Server: shareAuthMiddleware() adds auth data
|
|
557
|
+
// Client: access via props
|
|
558
|
+
const Layout: HonertiaPage<Props> = ({ auth, children }) => {
|
|
559
|
+
return (
|
|
560
|
+
<div>
|
|
561
|
+
{auth?.user ? (
|
|
562
|
+
<span>Welcome, {auth.user.name}</span>
|
|
563
|
+
) : (
|
|
564
|
+
<a href="/login">Login</a>
|
|
565
|
+
)}
|
|
566
|
+
{children}
|
|
567
|
+
</div>
|
|
568
|
+
)
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
## Architecture Notes
|
|
573
|
+
|
|
574
|
+
### Request-Scoped Services
|
|
575
|
+
|
|
576
|
+
Honertia creates a fresh Effect runtime per request via `effectBridge()`. This is required for Cloudflare Workers where I/O objects cannot be shared between requests.
|
|
577
|
+
|
|
578
|
+
```typescript
|
|
579
|
+
// This happens automatically in effectBridge middleware:
|
|
580
|
+
const layer = buildContextLayer(c) // Build layers from Hono context
|
|
581
|
+
const runtime = ManagedRuntime.make(layer) // New runtime per request
|
|
582
|
+
// ... handle request ...
|
|
583
|
+
await runtime.dispose() // Cleanup after request
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
This approach provides full type safety - your handlers declare their service requirements, and the type system ensures they're provided.
|
|
587
|
+
|
|
588
|
+
### Why Not Global Runtime?
|
|
589
|
+
|
|
590
|
+
On Cloudflare Workers, database connections and other I/O objects are isolated per request. Using a global runtime with `FiberRef` would lose type safety. The per-request runtime approach ensures:
|
|
591
|
+
|
|
592
|
+
1. Type-safe dependency injection
|
|
593
|
+
2. Proper resource cleanup
|
|
594
|
+
3. Full compatibility with Workers' isolation model
|
|
595
|
+
|
|
596
|
+
If you're using PlanetScale with Hyperdrive, the "connection" you create per request is lightweight - it's just a client pointing at Hyperdrive's persistent connection pool.
|
|
597
|
+
|
|
598
|
+
## Contributing
|
|
599
|
+
|
|
600
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
601
|
+
|
|
602
|
+
1. Fork the repository
|
|
603
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
604
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
605
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
606
|
+
5. Open a Pull Request
|
|
607
|
+
|
|
608
|
+
## License
|
|
609
|
+
|
|
610
|
+
MIT
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Honertia Auth
|
|
3
|
+
*
|
|
4
|
+
* Re-exports all authentication and authorization functionality.
|
|
5
|
+
* Import from 'honertia/auth' for auth-related functionality.
|
|
6
|
+
*/
|
|
7
|
+
export { RequireAuthLayer, RequireGuestLayer, isAuthenticated, currentUser, requireAuth, requireGuest, shareAuth, shareAuthMiddleware, effectAuthRoutes, loadUser, type AuthRoutesConfig, } from './effect/auth.js';
|
|
8
|
+
export { AuthService, AuthUserService, type AuthUser, } from './effect/services.js';
|
|
9
|
+
export { UnauthorizedError, ForbiddenError, } from './effect/errors.js';
|
|
10
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,WAAW,EACX,WAAW,EACX,YAAY,EACZ,SAAS,EACT,mBAAmB,EACnB,gBAAgB,EAChB,QAAQ,EACR,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAA;AAGzB,OAAO,EACL,WAAW,EACX,eAAe,EACf,KAAK,QAAQ,GACd,MAAM,sBAAsB,CAAA;AAG7B,OAAO,EACL,iBAAiB,EACjB,cAAc,GACf,MAAM,oBAAoB,CAAA"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Honertia Auth
|
|
3
|
+
*
|
|
4
|
+
* Re-exports all authentication and authorization functionality.
|
|
5
|
+
* Import from 'honertia/auth' for auth-related functionality.
|
|
6
|
+
*/
|
|
7
|
+
export { RequireAuthLayer, RequireGuestLayer, isAuthenticated, currentUser, requireAuth, requireGuest, shareAuth, shareAuthMiddleware, effectAuthRoutes, loadUser, } from './effect/auth.js';
|
|
8
|
+
// Re-export auth-related services
|
|
9
|
+
export { AuthService, AuthUserService, } from './effect/services.js';
|
|
10
|
+
// Re-export auth-related errors
|
|
11
|
+
export { UnauthorizedError, ForbiddenError, } from './effect/errors.js';
|