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.
@@ -5,9 +5,10 @@
5
5
  */
6
6
  import { Effect, Layer, Option } from 'effect';
7
7
  import { AuthUserService, AuthService, HonertiaService, RequestService } from './services.js';
8
- import { UnauthorizedError } from './errors.js';
8
+ import { UnauthorizedError, ValidationError } from './errors.js';
9
9
  import { effectRoutes } from './routing.js';
10
10
  import { render } from './responses.js';
11
+ import { validateRequest } from './validation.js';
11
12
  /**
12
13
  * Layer that requires an authenticated user.
13
14
  * Fails with UnauthorizedError if no user is present.
@@ -101,39 +102,65 @@ export function shareAuthMiddleware() {
101
102
  * effectAuthRoutes(app, {
102
103
  * loginComponent: 'Auth/Login',
103
104
  * registerComponent: 'Auth/Register',
105
+ * loginAction: loginUser,
106
+ * registerAction: registerUser,
104
107
  * })
105
108
  */
106
109
  export function effectAuthRoutes(app, config = {}) {
107
- const { loginPath = '/login', registerPath = '/register', logoutPath = '/logout', apiPath = '/api/auth', logoutRedirect = '/login', loginComponent = 'Auth/Login', registerComponent = 'Auth/Register', } = config;
110
+ const { loginPath = '/login', registerPath = '/register', logoutPath = '/logout', apiPath = '/api/auth', logoutRedirect = '/login', loginRedirect = '/', loginComponent = 'Auth/Login', registerComponent = 'Auth/Register', } = config;
108
111
  const routes = effectRoutes(app);
109
112
  // Login page (guest only)
110
113
  routes.get(loginPath, Effect.gen(function* () {
111
- yield* requireGuest(loginPath === '/login' ? '/' : loginPath);
114
+ yield* requireGuest(loginRedirect);
112
115
  return yield* render(loginComponent);
113
116
  }));
114
117
  // Register page (guest only)
115
118
  routes.get(registerPath, Effect.gen(function* () {
116
- yield* requireGuest(registerPath === '/register' ? '/' : registerPath);
119
+ yield* requireGuest(loginRedirect);
117
120
  return yield* render(registerComponent);
118
121
  }));
119
- // Logout (POST)
120
- routes.post(logoutPath, Effect.gen(function* () {
121
- const auth = yield* AuthService;
122
- const request = yield* RequestService;
123
- // Revoke session server-side
124
- yield* Effect.tryPromise(() => auth.api.signOut({
125
- headers: request.headers,
122
+ // Login action (POST) - guest only
123
+ if (config.loginAction) {
124
+ routes
125
+ .provide(RequireGuestLayer)
126
+ .post(loginPath, config.loginAction);
127
+ }
128
+ // Register action (POST) - guest only
129
+ if (config.registerAction) {
130
+ routes
131
+ .provide(RequireGuestLayer)
132
+ .post(registerPath, config.registerAction);
133
+ }
134
+ // Logout (POST) - use provided action or default
135
+ if (config.logoutAction) {
136
+ routes.post(logoutPath, config.logoutAction);
137
+ }
138
+ else {
139
+ routes.post(logoutPath, Effect.gen(function* () {
140
+ const auth = yield* AuthService;
141
+ const request = yield* RequestService;
142
+ // Revoke session server-side
143
+ yield* Effect.tryPromise(() => auth.api.signOut({
144
+ headers: request.headers,
145
+ }));
146
+ // Clear cookie and redirect
147
+ const sessionCookie = config.sessionCookie ?? 'better-auth.session_token';
148
+ return new Response(null, {
149
+ status: 303,
150
+ headers: {
151
+ 'Location': logoutRedirect,
152
+ 'Set-Cookie': `${sessionCookie}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; SameSite=Lax`,
153
+ },
154
+ });
126
155
  }));
127
- // Clear cookie and redirect
128
- const sessionCookie = config.sessionCookie ?? 'better-auth.session_token';
129
- return new Response(null, {
130
- status: 303,
131
- headers: {
132
- 'Location': logoutRedirect,
133
- 'Set-Cookie': `${sessionCookie}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; SameSite=Lax`,
134
- },
135
- });
136
- }));
156
+ }
157
+ // Additional guest-only actions (2FA, forgot password, etc.)
158
+ if (config.guestActions) {
159
+ const guestRoutes = routes.provide(RequireGuestLayer);
160
+ for (const [path, action] of Object.entries(config.guestActions)) {
161
+ guestRoutes.post(path, action);
162
+ }
163
+ }
137
164
  // Better-auth API handler (handles sign-in, sign-up, etc.)
138
165
  // Apply CORS if configured
139
166
  if (config.cors) {
@@ -202,3 +229,146 @@ export function loadUser(config = {}) {
202
229
  await next();
203
230
  };
204
231
  }
232
+ /**
233
+ * Create a better-auth form action with Honertia-friendly responses.
234
+ *
235
+ * Copies Set-Cookie headers from better-auth and redirects with 303.
236
+ * Maps errors into ValidationError so the standard error handler can render.
237
+ */
238
+ export function betterAuthFormAction(config) {
239
+ return Effect.gen(function* () {
240
+ const auth = yield* AuthService;
241
+ const request = yield* RequestService;
242
+ const input = yield* validateRequest(config.schema, {
243
+ errorComponent: config.errorComponent,
244
+ });
245
+ const result = yield* Effect.tryPromise({
246
+ try: () => config.call(auth, input, buildAuthRequest(request)),
247
+ catch: (error) => error,
248
+ }).pipe(Effect.mapError((error) => new ValidationError({
249
+ errors: (config.errorMapper ?? defaultAuthErrorMapper)(error),
250
+ component: config.errorComponent,
251
+ })));
252
+ const redirectTo = resolveRedirect(config.redirectTo, input, result);
253
+ const responseHeaders = new Headers({ Location: redirectTo });
254
+ const resultHeaders = getHeaders(result);
255
+ if (resultHeaders) {
256
+ appendSetCookies(responseHeaders, resultHeaders);
257
+ }
258
+ return new Response(null, {
259
+ status: 303,
260
+ headers: responseHeaders,
261
+ });
262
+ });
263
+ }
264
+ /**
265
+ * Create a better-auth logout action that clears cookies and redirects.
266
+ */
267
+ export function betterAuthLogoutAction(config = {}) {
268
+ return Effect.gen(function* () {
269
+ const auth = yield* AuthService;
270
+ const request = yield* RequestService;
271
+ const result = yield* Effect.tryPromise({
272
+ try: () => auth.api.signOut({
273
+ headers: request.headers,
274
+ request: buildAuthRequest(request),
275
+ returnHeaders: true,
276
+ }),
277
+ catch: () => undefined,
278
+ }).pipe(Effect.catchAll(() => Effect.succeed(undefined)));
279
+ const responseHeaders = new Headers({
280
+ Location: config.redirectTo ?? '/login',
281
+ });
282
+ const resultHeaders = getHeaders(result);
283
+ if (resultHeaders) {
284
+ appendSetCookies(responseHeaders, resultHeaders);
285
+ }
286
+ if (!responseHeaders.has('set-cookie')) {
287
+ appendLogoutCookies(responseHeaders, config.cookieNames);
288
+ }
289
+ return new Response(null, {
290
+ status: 303,
291
+ headers: responseHeaders,
292
+ });
293
+ });
294
+ }
295
+ function buildAuthRequest(request) {
296
+ return new Request(request.url, {
297
+ method: request.method,
298
+ headers: request.headers,
299
+ });
300
+ }
301
+ function resolveRedirect(target, input, result) {
302
+ if (typeof target === 'function') {
303
+ return target(input, result);
304
+ }
305
+ return target ?? '/';
306
+ }
307
+ function getHeaders(result) {
308
+ if (!result)
309
+ return undefined;
310
+ if (result instanceof Headers)
311
+ return result;
312
+ if (result instanceof Response)
313
+ return result.headers;
314
+ if (typeof result === 'object' && 'headers' in result && result.headers) {
315
+ return coerceHeaders(result.headers);
316
+ }
317
+ return undefined;
318
+ }
319
+ function coerceHeaders(value) {
320
+ return value instanceof Headers ? value : new Headers(value);
321
+ }
322
+ function defaultAuthErrorMapper(error) {
323
+ const message = getAuthErrorMessage(error) ?? 'Unable to complete request. Please try again.';
324
+ return { form: message };
325
+ }
326
+ function getAuthErrorMessage(error) {
327
+ if (!error || typeof error !== 'object')
328
+ return undefined;
329
+ const candidate = error;
330
+ if (typeof candidate.body?.message === 'string')
331
+ return candidate.body.message;
332
+ if (typeof candidate.message === 'string')
333
+ return candidate.message;
334
+ return undefined;
335
+ }
336
+ function appendSetCookies(target, source) {
337
+ const sourceWithSetCookie = source;
338
+ if (typeof sourceWithSetCookie.getSetCookie === 'function') {
339
+ for (const cookie of sourceWithSetCookie.getSetCookie()) {
340
+ target.append('set-cookie', cookie);
341
+ }
342
+ return;
343
+ }
344
+ const setCookie = source.get('set-cookie');
345
+ if (!setCookie) {
346
+ return;
347
+ }
348
+ // Split on cookie boundaries without breaking Expires attributes.
349
+ const parts = setCookie
350
+ .split(/,(?=[^;]+?=)/g)
351
+ .map((part) => part.trim())
352
+ .filter(Boolean);
353
+ for (const cookie of parts) {
354
+ target.append('set-cookie', cookie);
355
+ }
356
+ }
357
+ function appendExpiredCookie(target, name, options = {}) {
358
+ const base = `${name}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; SameSite=Lax`;
359
+ const value = options.secure ? `${base}; Secure` : base;
360
+ target.append('set-cookie', value);
361
+ }
362
+ function appendLogoutCookies(target, cookieNames) {
363
+ const defaults = [
364
+ 'better-auth.session_token',
365
+ 'better-auth.session_data',
366
+ 'better-auth.account_data',
367
+ 'better-auth.dont_remember',
368
+ ];
369
+ const names = cookieNames?.length ? cookieNames : defaults;
370
+ for (const name of names) {
371
+ appendExpiredCookie(target, name);
372
+ appendExpiredCookie(target, `__Secure-${name}`, { secure: true });
373
+ }
374
+ }
@@ -9,8 +9,8 @@ export * from './schema.js';
9
9
  export { getValidationData, formatSchemaErrors, validate, validateRequest, } from './validation.js';
10
10
  export { effectBridge, buildContextLayer, getEffectRuntime, type EffectBridgeConfig, } from './bridge.js';
11
11
  export { effectHandler, effect, handle, errorToResponse, } from './handler.js';
12
- export { effectAction, dbAction, authAction, simpleAction, injectUser, dbOperation, prepareData, preparedAction, } from './action.js';
12
+ export { action, authorize, dbTransaction, } from './action.js';
13
13
  export { redirect, render, renderWithErrors, json, text, notFound, forbidden, httpError, prefersJson, jsonOrRender, share, } from './responses.js';
14
14
  export { EffectRouteBuilder, effectRoutes, type EffectHandler, type BaseServices, } from './routing.js';
15
- export { RequireAuthLayer, RequireGuestLayer, isAuthenticated, currentUser, requireAuth, requireGuest, shareAuth, shareAuthMiddleware, effectAuthRoutes, loadUser, type AuthRoutesConfig, } from './auth.js';
15
+ export { RequireAuthLayer, RequireGuestLayer, isAuthenticated, currentUser, requireAuth, requireGuest, shareAuth, shareAuthMiddleware, effectAuthRoutes, betterAuthFormAction, betterAuthLogoutAction, loadUser, type AuthRoutesConfig, type BetterAuthFormActionConfig, type BetterAuthLogoutConfig, type BetterAuthActionResult, } from './auth.js';
16
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/effect/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EACL,eAAe,EACf,WAAW,EACX,eAAe,EACf,eAAe,EACf,cAAc,EACd,sBAAsB,EACtB,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,eAAe,GACrB,MAAM,eAAe,CAAA;AAGtB,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,aAAa,EACb,cAAc,EACd,SAAS,EACT,QAAQ,EACR,KAAK,QAAQ,GACd,MAAM,aAAa,CAAA;AAGpB,cAAc,aAAa,CAAA;AAG3B,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,QAAQ,EACR,eAAe,GAChB,MAAM,iBAAiB,CAAA;AAGxB,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,kBAAkB,GACxB,MAAM,aAAa,CAAA;AAGpB,OAAO,EACL,aAAa,EACb,MAAM,EACN,MAAM,EACN,eAAe,GAChB,MAAM,cAAc,CAAA;AAGrB,OAAO,EACL,YAAY,EACZ,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,UAAU,EACV,WAAW,EACX,WAAW,EACX,cAAc,GACf,MAAM,aAAa,CAAA;AAGpB,OAAO,EACL,QAAQ,EACR,MAAM,EACN,gBAAgB,EAChB,IAAI,EACJ,IAAI,EACJ,QAAQ,EACR,SAAS,EACT,SAAS,EACT,WAAW,EACX,YAAY,EACZ,KAAK,GACN,MAAM,gBAAgB,CAAA;AAGvB,OAAO,EACL,kBAAkB,EAClB,YAAY,EACZ,KAAK,aAAa,EAClB,KAAK,YAAY,GAClB,MAAM,cAAc,CAAA;AAGrB,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,WAAW,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/effect/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EACL,eAAe,EACf,WAAW,EACX,eAAe,EACf,eAAe,EACf,cAAc,EACd,sBAAsB,EACtB,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,eAAe,GACrB,MAAM,eAAe,CAAA;AAGtB,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,aAAa,EACb,cAAc,EACd,SAAS,EACT,QAAQ,EACR,KAAK,QAAQ,GACd,MAAM,aAAa,CAAA;AAGpB,cAAc,aAAa,CAAA;AAG3B,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,QAAQ,EACR,eAAe,GAChB,MAAM,iBAAiB,CAAA;AAGxB,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,kBAAkB,GACxB,MAAM,aAAa,CAAA;AAGpB,OAAO,EACL,aAAa,EACb,MAAM,EACN,MAAM,EACN,eAAe,GAChB,MAAM,cAAc,CAAA;AAGrB,OAAO,EACL,MAAM,EACN,SAAS,EACT,aAAa,GACd,MAAM,aAAa,CAAA;AAGpB,OAAO,EACL,QAAQ,EACR,MAAM,EACN,gBAAgB,EAChB,IAAI,EACJ,IAAI,EACJ,QAAQ,EACR,SAAS,EACT,SAAS,EACT,WAAW,EACX,YAAY,EACZ,KAAK,GACN,MAAM,gBAAgB,CAAA;AAGvB,OAAO,EACL,kBAAkB,EAClB,YAAY,EACZ,KAAK,aAAa,EAClB,KAAK,YAAY,GAClB,MAAM,cAAc,CAAA;AAGrB,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,0BAA0B,EAC/B,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,GAC5B,MAAM,WAAW,CAAA"}
@@ -15,11 +15,11 @@ export { getValidationData, formatSchemaErrors, validate, validateRequest, } fro
15
15
  export { effectBridge, buildContextLayer, getEffectRuntime, } from './bridge.js';
16
16
  // Handler
17
17
  export { effectHandler, effect, handle, errorToResponse, } from './handler.js';
18
- // Action Factories
19
- export { effectAction, dbAction, authAction, simpleAction, injectUser, dbOperation, prepareData, preparedAction, } from './action.js';
18
+ // Action Composables
19
+ export { action, authorize, dbTransaction, } from './action.js';
20
20
  // Response Helpers
21
21
  export { redirect, render, renderWithErrors, json, text, notFound, forbidden, httpError, prefersJson, jsonOrRender, share, } from './responses.js';
22
22
  // Routing
23
23
  export { EffectRouteBuilder, effectRoutes, } from './routing.js';
24
24
  // Auth
25
- export { RequireAuthLayer, RequireGuestLayer, isAuthenticated, currentUser, requireAuth, requireGuest, shareAuth, shareAuthMiddleware, effectAuthRoutes, loadUser, } from './auth.js';
25
+ export { RequireAuthLayer, RequireGuestLayer, isAuthenticated, currentUser, requireAuth, requireGuest, shareAuth, shareAuthMiddleware, effectAuthRoutes, betterAuthFormAction, betterAuthLogoutAction, loadUser, } from './auth.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "honertia",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Inertia.js-style server-driven SPA adapter for Hono",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",