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/README.md +526 -205
- package/dist/auth.d.ts +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +1 -1
- package/dist/effect/action.d.ts +43 -83
- package/dist/effect/action.d.ts.map +1 -1
- package/dist/effect/action.js +57 -116
- package/dist/effect/auth.d.ts +79 -3
- package/dist/effect/auth.d.ts.map +1 -1
- package/dist/effect/auth.js +191 -21
- package/dist/effect/index.d.ts +2 -2
- package/dist/effect/index.d.ts.map +1 -1
- package/dist/effect/index.js +3 -3
- package/package.json +1 -1
package/dist/effect/auth.js
CHANGED
|
@@ -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(
|
|
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(
|
|
119
|
+
yield* requireGuest(loginRedirect);
|
|
117
120
|
return yield* render(registerComponent);
|
|
118
121
|
}));
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
+
}
|
package/dist/effect/index.d.ts
CHANGED
|
@@ -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 {
|
|
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,
|
|
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"}
|
package/dist/effect/index.js
CHANGED
|
@@ -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
|
|
19
|
-
export {
|
|
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';
|