honertia 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/auth.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Re-exports all authentication and authorization functionality.
5
5
  * Import from 'honertia/auth' for auth-related functionality.
6
6
  */
7
- export { RequireAuthLayer, RequireGuestLayer, isAuthenticated, currentUser, requireAuth, requireGuest, shareAuth, shareAuthMiddleware, effectAuthRoutes, loadUser, type AuthRoutesConfig, } from './effect/auth.js';
7
+ export { RequireAuthLayer, RequireGuestLayer, isAuthenticated, currentUser, requireAuth, requireGuest, shareAuth, shareAuthMiddleware, effectAuthRoutes, betterAuthFormAction, betterAuthLogoutAction, loadUser, type AuthRoutesConfig, type AuthActionEffect, type BetterAuthFormActionConfig, type BetterAuthLogoutConfig, type BetterAuthActionResult, } from './effect/auth.js';
8
8
  export { AuthService, AuthUserService, type AuthUser, } from './effect/services.js';
9
9
  export { UnauthorizedError, ForbiddenError, } from './effect/errors.js';
10
10
  //# sourceMappingURL=auth.d.ts.map
@@ -1 +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"}
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,oBAAoB,EACpB,sBAAsB,EACtB,QAAQ,EACR,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,0BAA0B,EAC/B,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,GAC5B,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 CHANGED
@@ -4,7 +4,7 @@
4
4
  * Re-exports all authentication and authorization functionality.
5
5
  * Import from 'honertia/auth' for auth-related functionality.
6
6
  */
7
- export { RequireAuthLayer, RequireGuestLayer, isAuthenticated, currentUser, requireAuth, requireGuest, shareAuth, shareAuthMiddleware, effectAuthRoutes, loadUser, } from './effect/auth.js';
7
+ export { RequireAuthLayer, RequireGuestLayer, isAuthenticated, currentUser, requireAuth, requireGuest, shareAuth, shareAuthMiddleware, effectAuthRoutes, betterAuthFormAction, betterAuthLogoutAction, loadUser, } from './effect/auth.js';
8
8
  // Re-export auth-related services
9
9
  export { AuthService, AuthUserService, } from './effect/services.js';
10
10
  // Re-export auth-related errors
@@ -1,107 +1,67 @@
1
1
  /**
2
- * Effect Action Factories
2
+ * Effect Action Composables
3
3
  *
4
- * Pure function factories for creating Effect-based request handlers.
4
+ * Composable helpers for building Effect-based request handlers.
5
+ * Actions are fully opt-in - yield* only what you need.
5
6
  */
6
- import { Effect, Schema as S } from 'effect';
7
- import { DatabaseService, AuthUserService, RequestService, type AuthUser } from './services.js';
8
- import { ValidationError, UnauthorizedError } from './errors.js';
9
- import { Redirect } from './errors.js';
7
+ import { Effect } from 'effect';
8
+ import { DatabaseService, type AuthUser } from './services.js';
9
+ import { UnauthorizedError, ForbiddenError, Redirect } from './errors.js';
10
10
  /**
11
- * Create an Effect action with schema validation.
11
+ * Semantic wrapper for Effect actions.
12
+ *
13
+ * This is a minimal wrapper that marks an Effect as an action.
14
+ * All capabilities are opt-in via yield* inside your handler.
12
15
  *
13
16
  * @example
14
- * const createProject = effectAction(
15
- * S.Struct({ name: requiredString }),
16
- * (input) => Effect.gen(function* () {
17
+ * const createProject = action(
18
+ * Effect.gen(function* () {
19
+ * // Opt-in to authorization
20
+ * const auth = yield* authorize()
21
+ *
22
+ * // Opt-in to validation
23
+ * const input = yield* validateRequest(S.Struct({ name: requiredString }))
24
+ *
25
+ * // Opt-in to database
17
26
  * const db = yield* DatabaseService
18
- * yield* Effect.tryPromise(() => db.insert(projects).values(input))
19
- * return new Redirect({ url: '/projects', status: 303 })
20
- * })
21
- * )
22
- */
23
- export declare function effectAction<A, I, R, E>(schema: S.Schema<A, I>, handler: (input: A) => Effect.Effect<Response | Redirect, E, R>, options?: {
24
- errorComponent?: string;
25
- messages?: Record<string, string>;
26
- attributes?: Record<string, string>;
27
- }): Effect.Effect<Response | Redirect, E | ValidationError, R | RequestService>;
28
- /**
29
- * Create an Effect action that requires authentication and database access.
30
27
  *
31
- * @example
32
- * const createProject = dbAction(
33
- * S.Struct({ name: requiredString }),
34
- * (input, { db, user }) => Effect.gen(function* () {
35
28
  * yield* Effect.tryPromise(() =>
36
- * db.insert(projects).values({ ...input, userId: user.user.id })
29
+ * db.insert(projects).values({ ...input, userId: auth.user.id })
37
30
  * )
38
31
  * return new Redirect({ url: '/projects', status: 303 })
39
32
  * })
40
33
  * )
41
34
  */
42
- export declare function dbAction<A, I, E>(schema: S.Schema<A, I>, handler: (input: A, deps: {
43
- db: unknown;
44
- user: AuthUser;
45
- }) => Effect.Effect<Response | Redirect, E, never>, options?: {
46
- errorComponent?: string;
47
- messages?: Record<string, string>;
48
- attributes?: Record<string, string>;
49
- }): Effect.Effect<Response | Redirect, E | ValidationError | UnauthorizedError, RequestService | DatabaseService | AuthUserService>;
35
+ export declare function action<R, E>(handler: Effect.Effect<Response | Redirect, E, R>): Effect.Effect<Response | Redirect, E, R>;
50
36
  /**
51
- * Create an Effect action that requires authentication.
37
+ * Authorization helper - opt-in to auth check.
52
38
  *
53
- * @example
54
- * const showDashboard = authAction(() => Effect.gen(function* () {
55
- * const user = yield* AuthUserService
56
- * const honertia = yield* HonertiaService
57
- * return yield* Effect.tryPromise(() => honertia.render('Dashboard', { user: user.user }))
58
- * }))
59
- */
60
- export declare function authAction<R, E>(handler: (user: AuthUser) => Effect.Effect<Response | Redirect, E, R>): Effect.Effect<Response | Redirect, E | UnauthorizedError, R | AuthUserService>;
61
- /**
62
- * Create a simple Effect action without validation.
39
+ * Returns the authenticated user if authorized.
40
+ * Fails with UnauthorizedError if no user is present.
41
+ * Fails with ForbiddenError if the check returns false.
42
+ * The check function is optional - if not provided, just requires authentication.
63
43
  *
64
44
  * @example
65
- * const listProjects = simpleAction(() => Effect.gen(function* () {
66
- * const db = yield* DatabaseService
67
- * const user = yield* AuthUserService
68
- * const projects = yield* Effect.tryPromise(() => db.query.projects.findMany())
69
- * const honertia = yield* HonertiaService
70
- * return yield* Effect.tryPromise(() => honertia.render('Projects', { projects }))
71
- * }))
45
+ * // Just require authentication
46
+ * const auth = yield* authorize()
47
+ *
48
+ * // Require specific role (if your user type has a role field)
49
+ * const auth = yield* authorize((a) => a.user.role === 'admin')
50
+ *
51
+ * // Require resource ownership
52
+ * const auth = yield* authorize((a) => a.user.id === project.userId)
72
53
  */
73
- export declare function simpleAction<R, E>(handler: () => Effect.Effect<Response | Redirect, E, R>): Effect.Effect<Response | Redirect, E, R>;
54
+ export declare function authorize(check?: (user: AuthUser) => boolean): Effect.Effect<AuthUser, UnauthorizedError | ForbiddenError, never>;
74
55
  /**
75
- * Inject additional data into the validated input.
56
+ * Run multiple database operations in a transaction.
57
+ * Automatically rolls back on any failure.
76
58
  *
77
59
  * @example
78
- * const createProject = effectAction(
79
- * S.Struct({ name: requiredString }),
80
- * (input) => pipe(
81
- * injectUser(input),
82
- * Effect.flatMap(({ name, userId }) =>
83
- * Effect.tryPromise(() => db.insert(projects).values({ name, userId }))
84
- * )
85
- * )
86
- * )
87
- */
88
- export declare function injectUser<T extends Record<string, unknown>>(input: T): Effect.Effect<T & {
89
- userId: string;
90
- }, UnauthorizedError, AuthUserService>;
91
- /**
92
- * Run a database operation wrapped in Effect.
93
- */
94
- export declare function dbOperation<T>(operation: (db: unknown) => Promise<T>): Effect.Effect<T, Error, DatabaseService>;
95
- /**
96
- * Prepare validation data by transforming it before validation.
97
- */
98
- export declare function prepareData<T extends Record<string, unknown>>(transform: (data: Record<string, unknown>) => T | Promise<T>): Effect.Effect<T, never, RequestService>;
99
- /**
100
- * Create an action with custom data preparation.
60
+ * yield* dbTransaction(async (tx) => {
61
+ * await tx.insert(users).values({ name: 'Alice' })
62
+ * await tx.update(accounts).set({ balance: 100 }).where(eq(accounts.userId, id))
63
+ * return { success: true }
64
+ * })
101
65
  */
102
- export declare function preparedAction<A, I, R, E>(schema: S.Schema<A, I>, prepare: (data: Record<string, unknown>) => Record<string, unknown> | Promise<Record<string, unknown>>, handler: (input: A) => Effect.Effect<Response | Redirect, E, R>, options?: {
103
- errorComponent?: string;
104
- messages?: Record<string, string>;
105
- attributes?: Record<string, string>;
106
- }): Effect.Effect<Response | Redirect, E | ValidationError, R | RequestService>;
66
+ export declare function dbTransaction<T>(operations: (tx: unknown) => Promise<T>): Effect.Effect<T, Error, DatabaseService>;
107
67
  //# sourceMappingURL=action.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"action.d.ts","sourceRoot":"","sources":["../../src/effect/action.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"action.d.ts","sourceRoot":"","sources":["../../src/effect/action.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAA;AACvC,OAAO,EACL,eAAe,EAEf,KAAK,QAAQ,EACd,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAEzE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,CAAC,EACzB,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,GAAG,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC,GAChD,MAAM,CAAC,MAAM,CAAC,QAAQ,GAAG,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC,CAE1C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,SAAS,CACvB,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,OAAO,GAClC,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,iBAAiB,GAAG,cAAc,EAAE,KAAK,CAAC,CAoBpE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAC7B,UAAU,EAAE,CAAC,EAAE,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,GACtC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,eAAe,CAAC,CAQ1C"}
@@ -1,150 +1,91 @@
1
1
  /**
2
- * Effect Action Factories
2
+ * Effect Action Composables
3
3
  *
4
- * Pure function factories for creating Effect-based request handlers.
4
+ * Composable helpers for building Effect-based request handlers.
5
+ * Actions are fully opt-in - yield* only what you need.
5
6
  */
6
- import { Effect } from 'effect';
7
+ import { Effect, Option } from 'effect';
7
8
  import { DatabaseService, AuthUserService, } from './services.js';
8
- import { validateRequest, getValidationData, validate } from './validation.js';
9
+ import { UnauthorizedError, ForbiddenError } from './errors.js';
9
10
  /**
10
- * Create an Effect action with schema validation.
11
+ * Semantic wrapper for Effect actions.
12
+ *
13
+ * This is a minimal wrapper that marks an Effect as an action.
14
+ * All capabilities are opt-in via yield* inside your handler.
11
15
  *
12
16
  * @example
13
- * const createProject = effectAction(
14
- * S.Struct({ name: requiredString }),
15
- * (input) => Effect.gen(function* () {
17
+ * const createProject = action(
18
+ * Effect.gen(function* () {
19
+ * // Opt-in to authorization
20
+ * const auth = yield* authorize()
21
+ *
22
+ * // Opt-in to validation
23
+ * const input = yield* validateRequest(S.Struct({ name: requiredString }))
24
+ *
25
+ * // Opt-in to database
16
26
  * const db = yield* DatabaseService
17
- * yield* Effect.tryPromise(() => db.insert(projects).values(input))
18
- * return new Redirect({ url: '/projects', status: 303 })
19
- * })
20
- * )
21
- */
22
- export function effectAction(schema, handler, options) {
23
- return Effect.gen(function* () {
24
- const input = yield* validateRequest(schema, {
25
- errorComponent: options?.errorComponent,
26
- messages: options?.messages,
27
- attributes: options?.attributes,
28
- });
29
- return yield* handler(input);
30
- });
31
- }
32
- /**
33
- * Create an Effect action that requires authentication and database access.
34
27
  *
35
- * @example
36
- * const createProject = dbAction(
37
- * S.Struct({ name: requiredString }),
38
- * (input, { db, user }) => Effect.gen(function* () {
39
28
  * yield* Effect.tryPromise(() =>
40
- * db.insert(projects).values({ ...input, userId: user.user.id })
29
+ * db.insert(projects).values({ ...input, userId: auth.user.id })
41
30
  * )
42
31
  * return new Redirect({ url: '/projects', status: 303 })
43
32
  * })
44
33
  * )
45
34
  */
46
- export function dbAction(schema, handler, options) {
47
- return Effect.gen(function* () {
48
- const db = yield* DatabaseService;
49
- const user = yield* AuthUserService;
50
- const input = yield* validateRequest(schema, {
51
- errorComponent: options?.errorComponent,
52
- messages: options?.messages,
53
- attributes: options?.attributes,
54
- });
55
- return yield* handler(input, { db, user });
56
- });
35
+ export function action(handler) {
36
+ return handler;
57
37
  }
58
38
  /**
59
- * Create an Effect action that requires authentication.
39
+ * Authorization helper - opt-in to auth check.
60
40
  *
61
- * @example
62
- * const showDashboard = authAction(() => Effect.gen(function* () {
63
- * const user = yield* AuthUserService
64
- * const honertia = yield* HonertiaService
65
- * return yield* Effect.tryPromise(() => honertia.render('Dashboard', { user: user.user }))
66
- * }))
67
- */
68
- export function authAction(handler) {
69
- return Effect.gen(function* () {
70
- const user = yield* AuthUserService;
71
- return yield* handler(user);
72
- });
73
- }
74
- /**
75
- * Create a simple Effect action without validation.
41
+ * Returns the authenticated user if authorized.
42
+ * Fails with UnauthorizedError if no user is present.
43
+ * Fails with ForbiddenError if the check returns false.
44
+ * The check function is optional - if not provided, just requires authentication.
76
45
  *
77
46
  * @example
78
- * const listProjects = simpleAction(() => Effect.gen(function* () {
79
- * const db = yield* DatabaseService
80
- * const user = yield* AuthUserService
81
- * const projects = yield* Effect.tryPromise(() => db.query.projects.findMany())
82
- * const honertia = yield* HonertiaService
83
- * return yield* Effect.tryPromise(() => honertia.render('Projects', { projects }))
84
- * }))
85
- */
86
- export function simpleAction(handler) {
87
- return handler();
88
- }
89
- /**
90
- * Inject additional data into the validated input.
47
+ * // Just require authentication
48
+ * const auth = yield* authorize()
91
49
  *
92
- * @example
93
- * const createProject = effectAction(
94
- * S.Struct({ name: requiredString }),
95
- * (input) => pipe(
96
- * injectUser(input),
97
- * Effect.flatMap(({ name, userId }) =>
98
- * Effect.tryPromise(() => db.insert(projects).values({ name, userId }))
99
- * )
100
- * )
101
- * )
50
+ * // Require specific role (if your user type has a role field)
51
+ * const auth = yield* authorize((a) => a.user.role === 'admin')
52
+ *
53
+ * // Require resource ownership
54
+ * const auth = yield* authorize((a) => a.user.id === project.userId)
102
55
  */
103
- export function injectUser(input) {
56
+ export function authorize(check) {
104
57
  return Effect.gen(function* () {
105
- const authUser = yield* AuthUserService;
106
- return { ...input, userId: authUser.user.id };
58
+ const maybeUser = yield* Effect.serviceOption(AuthUserService);
59
+ if (Option.isNone(maybeUser)) {
60
+ return yield* Effect.fail(new UnauthorizedError({
61
+ message: 'Authentication required',
62
+ redirectTo: '/login',
63
+ }));
64
+ }
65
+ const user = maybeUser.value;
66
+ if (check && !check(user)) {
67
+ return yield* Effect.fail(new ForbiddenError({ message: 'Not authorized' }));
68
+ }
69
+ return user;
107
70
  });
108
71
  }
109
72
  /**
110
- * Run a database operation wrapped in Effect.
73
+ * Run multiple database operations in a transaction.
74
+ * Automatically rolls back on any failure.
75
+ *
76
+ * @example
77
+ * yield* dbTransaction(async (tx) => {
78
+ * await tx.insert(users).values({ name: 'Alice' })
79
+ * await tx.update(accounts).set({ balance: 100 }).where(eq(accounts.userId, id))
80
+ * return { success: true }
81
+ * })
111
82
  */
112
- export function dbOperation(operation) {
83
+ export function dbTransaction(operations) {
113
84
  return Effect.gen(function* () {
114
85
  const db = yield* DatabaseService;
115
86
  return yield* Effect.tryPromise({
116
- try: () => operation(db),
87
+ try: () => db.transaction(operations),
117
88
  catch: (error) => error instanceof Error ? error : new Error(String(error)),
118
89
  });
119
90
  });
120
91
  }
121
- /**
122
- * Prepare validation data by transforming it before validation.
123
- */
124
- export function prepareData(transform) {
125
- return Effect.gen(function* () {
126
- const data = yield* getValidationData;
127
- return yield* Effect.tryPromise({
128
- try: () => Promise.resolve(transform(data)),
129
- catch: () => new Error('Transform failed'),
130
- }).pipe(Effect.catchAll(() => Effect.succeed(data)));
131
- });
132
- }
133
- /**
134
- * Create an action with custom data preparation.
135
- */
136
- export function preparedAction(schema, prepare, handler, options) {
137
- return Effect.gen(function* () {
138
- const rawData = yield* getValidationData;
139
- const preparedData = yield* Effect.tryPromise({
140
- try: () => Promise.resolve(prepare(rawData)),
141
- catch: () => new Error('Prepare failed'),
142
- }).pipe(Effect.catchAll(() => Effect.succeed(rawData)));
143
- const input = yield* validate(schema, {
144
- errorComponent: options?.errorComponent,
145
- messages: options?.messages,
146
- attributes: options?.attributes,
147
- })(preparedData);
148
- return yield* handler(input);
149
- });
150
- }
@@ -3,10 +3,11 @@
3
3
  *
4
4
  * Authentication and authorization via Effect Layers.
5
5
  */
6
- import { Effect, Layer } from 'effect';
6
+ import { Effect, Layer, Schema as S } from 'effect';
7
7
  import type { Hono, MiddlewareHandler, Env } from 'hono';
8
- import { AuthUserService, HonertiaService, type AuthUser } from './services.js';
9
- import { UnauthorizedError } from './errors.js';
8
+ import { AuthUserService, AuthService, HonertiaService, RequestService, type AuthUser } from './services.js';
9
+ import { UnauthorizedError, ValidationError } from './errors.js';
10
+ import { type EffectHandler } from './routing.js';
10
11
  /**
11
12
  * Layer that requires an authenticated user.
12
13
  * Fails with UnauthorizedError if no user is present.
@@ -51,6 +52,11 @@ export declare const shareAuth: Effect.Effect<void, never, HonertiaService>;
51
52
  * Middleware version of shareAuth for use with app.use().
52
53
  */
53
54
  export declare function shareAuthMiddleware<E extends Env>(): MiddlewareHandler<E>;
55
+ /**
56
+ * An auth action effect that returns a Response.
57
+ * Used for loginAction, registerAction, logoutAction, and guestActions.
58
+ */
59
+ export type AuthActionEffect<R = never, E extends Error = Error> = EffectHandler<R, E>;
54
60
  /**
55
61
  * Configuration for auth routes.
56
62
  */
@@ -60,6 +66,9 @@ export interface AuthRoutesConfig<E extends Env> {
60
66
  logoutPath?: string;
61
67
  apiPath?: string;
62
68
  logoutRedirect?: string;
69
+ /**
70
+ * Redirect path for authenticated users hitting login/register pages.
71
+ */
63
72
  loginRedirect?: string;
64
73
  loginComponent?: string;
65
74
  registerComponent?: string;
@@ -72,6 +81,37 @@ export interface AuthRoutesConfig<E extends Env> {
72
81
  origin: string | string[] | ((origin: string) => string | undefined | null);
73
82
  credentials?: boolean;
74
83
  };
84
+ /**
85
+ * POST handler for login form submission.
86
+ * Automatically wrapped with RequireGuestLayer.
87
+ * Use betterAuthFormAction to create this.
88
+ */
89
+ loginAction?: AuthActionEffect;
90
+ /**
91
+ * POST handler for registration form submission.
92
+ * Automatically wrapped with RequireGuestLayer.
93
+ * Use betterAuthFormAction to create this.
94
+ */
95
+ registerAction?: AuthActionEffect;
96
+ /**
97
+ * POST handler for logout.
98
+ * If not provided, uses a default handler that calls auth.api.signOut.
99
+ * Use betterAuthLogoutAction to create this.
100
+ */
101
+ logoutAction?: AuthActionEffect;
102
+ /**
103
+ * Additional guest-only POST routes for extended auth flows.
104
+ * Keys are paths (e.g., '/forgot-password'), values are Effect handlers.
105
+ * All routes are wrapped with RequireGuestLayer.
106
+ *
107
+ * @example
108
+ * guestActions: {
109
+ * '/forgot-password': forgotPasswordAction,
110
+ * '/reset-password': resetPasswordAction,
111
+ * '/login/2fa': verify2FAAction,
112
+ * }
113
+ */
114
+ guestActions?: Record<string, AuthActionEffect>;
75
115
  }
76
116
  /**
77
117
  * Register standard auth routes.
@@ -80,6 +120,8 @@ export interface AuthRoutesConfig<E extends Env> {
80
120
  * effectAuthRoutes(app, {
81
121
  * loginComponent: 'Auth/Login',
82
122
  * registerComponent: 'Auth/Register',
123
+ * loginAction: loginUser,
124
+ * registerAction: registerUser,
83
125
  * })
84
126
  */
85
127
  export declare function effectAuthRoutes<E extends Env>(app: Hono<E>, config?: AuthRoutesConfig<E>): void;
@@ -91,4 +133,38 @@ export declare function loadUser<E extends Env>(config?: {
91
133
  userKey?: string;
92
134
  sessionCookie?: string;
93
135
  }): MiddlewareHandler<E>;
136
+ /**
137
+ * Result types from better-auth calls that expose headers.
138
+ */
139
+ export type BetterAuthActionResult = Response | Headers | {
140
+ headers?: Headers | HeadersInit;
141
+ };
142
+ /**
143
+ * Config for better-auth form actions (login/register).
144
+ */
145
+ export interface BetterAuthFormActionConfig<A, I, AuthClient = unknown> {
146
+ schema: S.Schema<A, I>;
147
+ errorComponent: string;
148
+ call: (auth: AuthClient, input: A, request: Request) => Promise<BetterAuthActionResult>;
149
+ errorMapper?: (error: unknown) => Record<string, string>;
150
+ redirectTo?: string | ((input: A, result: BetterAuthActionResult) => string);
151
+ }
152
+ /**
153
+ * Create a better-auth form action with Honertia-friendly responses.
154
+ *
155
+ * Copies Set-Cookie headers from better-auth and redirects with 303.
156
+ * Maps errors into ValidationError so the standard error handler can render.
157
+ */
158
+ export declare function betterAuthFormAction<A, I, AuthClient = unknown>(config: BetterAuthFormActionConfig<A, I, AuthClient>): Effect.Effect<Response, ValidationError, RequestService | AuthService>;
159
+ /**
160
+ * Config for better-auth logout actions.
161
+ */
162
+ export interface BetterAuthLogoutConfig {
163
+ redirectTo?: string;
164
+ cookieNames?: string[];
165
+ }
166
+ /**
167
+ * Create a better-auth logout action that clears cookies and redirects.
168
+ */
169
+ export declare function betterAuthLogoutAction(config?: BetterAuthLogoutConfig): Effect.Effect<Response, never, RequestService | AuthService>;
94
170
  //# sourceMappingURL=auth.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/effect/auth.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,EAAE,KAAK,EAAU,MAAM,QAAQ,CAAA;AAC9C,OAAO,KAAK,EAAE,IAAI,EAAE,iBAAiB,EAAE,GAAG,EAAE,MAAM,MAAM,CAAA;AACxD,OAAO,EAAE,eAAe,EAAe,eAAe,EAAkB,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAA;AAC5G,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAI/C;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,wDAiB5B,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,8CAe7B,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,KAAK,CACM,CAAA;AAEvE;;GAEG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,CAGlE,CAAA;AAEH;;GAEG;AACH,eAAO,MAAM,WAAW,GACtB,mBAAqB,KACpB,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,iBAAiB,EAAE,KAAK,CAQhD,CAAA;AAEH;;GAEG;AACH,eAAO,MAAM,YAAY,GACvB,mBAAgB,KACf,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,iBAAiB,EAAE,KAAK,CAQ5C,CAAA;AAEH;;GAEG;AACH,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,eAAe,CAK9D,CAAA;AAEJ;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,GAAG,KAAK,iBAAiB,CAAC,CAAC,CAAC,CASzE;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB,CAAC,CAAC,SAAS,GAAG;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;OAGG;IACH,IAAI,CAAC,EAAE;QACL,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC,CAAA;QAC3E,WAAW,CAAC,EAAE,OAAO,CAAA;KACtB,CAAA;CACF;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,GAAG,EAC5C,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,EACZ,MAAM,GAAE,gBAAgB,CAAC,CAAC,CAAM,GAC/B,IAAI,CAoGN;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,GAAG,EACpC,MAAM,GAAE;IACN,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,CAAA;CAClB,GACL,iBAAiB,CAAC,CAAC,CAAC,CAwBtB"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/effect/auth.ts"],"names":[],"mappings":"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"}