feat(core)!: introduce runtime extension registry and flat module conventions

BREAKING CHANGE: sup config now derives entries from package.json#exports and a server/client glob instead of manual lists; module structure follows flat + barrel convention with .server.js/.client.js runtime suffixes
This commit is contained in:
2026-04-22 14:13:30 -04:00
parent 61388f04a6
commit 0106bc4ea0
35 changed files with 917 additions and 528 deletions
+85 -8
View File
@@ -1,12 +1,89 @@
'use client';
/**
* Auth Pages Export for Next.js App Router
*
* This exports the auth client components.
* Users must create their own server component wrapper that imports the actions.
*/
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import LoginPage from './pages/LoginPage.client.js';
import RegisterPage from './pages/RegisterPage.client.js';
import ForgotPasswordPage from './pages/ForgotPasswordPage.client.js';
import ResetPasswordPage from './pages/ResetPasswordPage.client.js';
import ConfirmEmailPage from './pages/ConfirmEmailPage.client.js';
import LogoutPage from './pages/LogoutPage.client.js';
export { default as AuthPagesClient } from './components/AuthPages.js';
export { default as AuthPagesLayout } from './components/AuthPagesLayout.js';
const PAGE_COMPONENTS = {
login: LoginPage,
register: RegisterPage,
forgot: ForgotPasswordPage,
reset: ResetPasswordPage,
confirm: ConfirmEmailPage,
logout: LogoutPage,
};
export default function AuthPage({
params,
searchParams,
registerAction,
loginAction,
forgotPasswordAction,
resetPasswordAction,
verifyEmailAction,
logoutAction,
setSessionCookieAction,
redirectAfterLogin = '/',
currentUser = null,
}) {
const router = useRouter();
const [currentPage, setCurrentPage] = useState(null);
const [email, setEmail] = useState('');
const [token, setToken] = useState('');
useEffect(() => {
const fromParams = params?.auth?.[0];
if (fromParams) { setCurrentPage(fromParams); return; }
if (typeof window !== 'undefined') {
const match = window.location.pathname.match(/\/auth\/([^\/?]+)/);
setCurrentPage(match ? match[1] : 'login');
} else {
setCurrentPage('login');
}
}, [params]);
useEffect(() => {
const run = async () => {
// searchParams became a Promise in Next.js 15 — resolve both forms.
const resolved = searchParams && typeof searchParams.then === 'function'
? await searchParams
: searchParams;
const urlParams = typeof window !== 'undefined'
? new URLSearchParams(window.location.search)
: null;
setEmail(resolved?.email || urlParams?.get('email') || '');
setToken(resolved?.token || urlParams?.get('token') || '');
};
run();
}, [searchParams]);
const navigate = (page) => router.push(`/auth/${page}`);
if (!currentPage) return null;
const Page = PAGE_COMPONENTS[currentPage] || LoginPage;
const common = { onNavigate: navigate, currentUser };
switch (Page) {
case LoginPage:
return <Page {...common} onSubmit={loginAction} onSetSessionCookie={setSessionCookieAction} redirectAfterLogin={redirectAfterLogin} />;
case RegisterPage:
return <Page {...common} onSubmit={registerAction} />;
case ForgotPasswordPage:
return <Page {...common} onSubmit={forgotPasswordAction} />;
case ResetPasswordPage:
return <Page {...common} onSubmit={resetPasswordAction} email={email} token={token} />;
case ConfirmEmailPage:
return <Page {...common} onSubmit={verifyEmailAction} email={email} token={token} />;
case LogoutPage:
return <Page onLogout={logoutAction} onSetSessionCookie={setSessionCookieAction} />;
default:
return null;
}
}
+5 -16
View File
@@ -1,15 +1,4 @@
/**
* Auth Page - Server Component Wrapper for Next.js App Router
*
* Default auth UI: login, register, forgot, reset, confirm, logout at /auth/[...auth].
* Re-export in your app: export { default } from '@zen/core/features/auth/page';
*
* For custom auth pages (all flows) that match your site style, use components from
* '@zen/core/features/auth/components' and actions from '@zen/core/features/auth/actions'.
* See README-custom-login.md in this package. Basic sites can keep using this default page.
*/
import { AuthPagesClient } from '@zen/core/features/auth/pages';
import AuthPageClient from './AuthPage.client.js';
import {
registerAction,
loginAction,
@@ -18,16 +7,16 @@ import {
resetPasswordAction,
verifyEmailAction,
setSessionCookie,
getSession
} from '@zen/core/features/auth/actions';
getSession,
} from './actions.js';
export default async function AuthPage({ params, searchParams }) {
const session = await getSession();
return (
<div className="min-h-screen flex items-center justify-center p-4 md:p-8">
<div className="max-w-md w-full">
<AuthPagesClient
<AuthPageClient
params={params}
searchParams={searchParams}
registerAction={registerAction}
+358 -16
View File
@@ -1,19 +1,361 @@
/**
* Server Actions Export
* This file ONLY exports server actions - no client components
*/
'use server';
export {
registerAction,
loginAction,
logoutAction,
getSession,
forgotPasswordAction,
resetPasswordAction,
verifyEmailAction,
setSessionCookie,
refreshSessionCookie
} from './actions/authActions.js';
import { register, login, requestPasswordReset, resetPassword, verifyUserEmail } from './auth.js';
import { validateSession, deleteSession } from './session.js';
import { verifyEmailToken, verifyResetToken, sendVerificationEmail, sendPasswordResetEmail } from './email.js';
import { fail } from '@zen/core/shared/logger';
import { cookies, headers } from 'next/headers';
import { getSessionCookieName, getPublicBaseUrl } from '@zen/core/shared/config';
import { checkRateLimit, getIpFromHeaders, formatRetryAfter } from '@zen/core/shared/rate-limit';
/**
* Errors that are safe to surface verbatim to the client (e.g. "token expired").
* All other errors — including library-layer and database errors — must be caught,
* logged server-side only, and replaced with a generic message to prevent internal
* detail disclosure.
*/
class UserFacingError extends Error {
constructor(message) {
super(message);
this.name = 'UserFacingError';
}
}
async function getClientIp() {
const h = await headers();
return getIpFromHeaders(h);
}
// Emitted at most once per process lifetime to avoid log flooding while still
// alerting operators that per-IP rate limiting is inactive for server actions.
let _rateLimitUnavailableWarned = false;
/**
* Apply per-IP rate limiting only when a real IP address is available.
*
* When IP resolves to 'unknown' (no trusted proxy configured), every caller
* shares the single bucket keyed '<action>:unknown'. A single attacker can
* exhaust that bucket in 5 requests and impose a 30-minute denial-of-service
* on every legitimate user. Rate limiting is therefore suspended for the
* 'unknown' case and a one-time operator warning is emitted instead,
* mirroring the identical policy applied to API routes in router.js.
*/
function enforceRateLimit(ip, action) {
if (ip === 'unknown') {
if (!_rateLimitUnavailableWarned) {
_rateLimitUnavailableWarned = true;
fail(
'Rate limiting inactive (server actions): client IP cannot be determined. ' +
'Set ZEN_TRUST_PROXY=true behind a verified reverse proxy to enable per-IP rate limiting.'
);
}
return null;
}
return checkRateLimit(ip, action);
}
/**
* Validate anti-bot fields submitted with forms.
* - _hp : honeypot field — must be empty
* - _t : form load timestamp (ms) — submission must be at least 1.5 s after page
* load AND no more than MAX_FORM_AGE_MS in the past. Both a lower bound
* (prevents instant automated submission) and an upper bound (prevents the
* trivial bypass of supplying an arbitrary past timestamp such as _t=1) are
* enforced. Future timestamps are also rejected.
*/
function validateAntiBotFields(formData) {
const honeypot = formData.get('_hp');
if (honeypot && honeypot.length > 0) {
return { valid: false, error: 'Requête invalide' };
}
const MIN_ELAPSED_MS = 1_500;
const MAX_FORM_AGE_MS = 10 * 60 * 1_000;
const now = Date.now();
const t = parseInt(formData.get('_t') || '0', 10);
const elapsed = now - t;
if (t === 0 || t > now || elapsed < MIN_ELAPSED_MS || elapsed > MAX_FORM_AGE_MS) {
return { valid: false, error: 'Requête invalide' };
}
return { valid: true };
}
export const COOKIE_NAME = getSessionCookieName();
export async function registerAction(formData) {
try {
const botCheck = validateAntiBotFields(formData);
if (!botCheck.valid) return { success: false, error: botCheck.error };
const ip = await getClientIp();
const rl = enforceRateLimit(ip, 'register');
if (rl && !rl.allowed) {
return { success: false, error: `Trop de tentatives. Réessayez dans ${formatRetryAfter(rl.retryAfterMs)}.` };
}
const email = formData.get('email');
const password = formData.get('password');
const name = formData.get('name');
const result = await register({ email, password, name });
await sendVerificationEmail(result.user.email, result.verificationToken, getPublicBaseUrl());
return {
success: true,
message: 'Compte créé avec succès. Consultez votre e-mail pour vérifier votre compte.',
user: result.user
};
} catch (error) {
// Never return raw error.message to the client — library and database errors
// (e.g. unique-constraint violations) expose internal table names and schema.
fail(`Auth: registerAction error: ${error.message}`);
return { success: false, error: 'Une erreur interne est survenue. Veuillez réessayer.' };
}
}
export async function loginAction(formData) {
try {
const botCheck = validateAntiBotFields(formData);
if (!botCheck.valid) return { success: false, error: botCheck.error };
const ip = await getClientIp();
const rl = enforceRateLimit(ip, 'login');
if (rl && !rl.allowed) {
return { success: false, error: `Trop de tentatives. Réessayez dans ${formatRetryAfter(rl.retryAfterMs)}.` };
}
const email = formData.get('email');
const password = formData.get('password');
const result = await login({ email, password });
// An HttpOnly cookie is the only safe transport for session tokens; setting it
// here keeps the token out of any JavaScript-readable response payload.
const cookieStore = await cookies();
cookieStore.set(COOKIE_NAME, result.session.token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 30,
path: '/'
});
return {
success: true,
message: 'Connexion réussie',
user: result.user
};
} catch (error) {
fail(`Auth: loginAction error: ${error.message}`);
return { success: false, error: 'Identifiants invalides ou erreur interne. Veuillez réessayer.' };
}
}
/**
* Set session cookie after verifying the token is a genuine live session.
*
* Client-callable. Without server-side token validation an attacker could
* supply any arbitrary string (including a stolen token for another user)
* and have it written as the HttpOnly session cookie, bypassing the protection
* HttpOnly is intended to provide. The token is therefore validated against
* the session store before the cookie is written.
*/
export async function setSessionCookie(token) {
try {
if (!token || typeof token !== 'string' || token.trim() === '') {
return { success: false, error: 'Jeton de session invalide' };
}
const session = await validateSession(token);
if (!session) {
return { success: false, error: 'Session invalide ou expirée' };
}
const cookieStore = await cookies();
cookieStore.set(COOKIE_NAME, token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 30,
path: '/'
});
return { success: true };
} catch (error) {
fail(`Auth: setSessionCookie error: ${error.message}`);
return { success: false, error: 'Une erreur interne est survenue' };
}
}
/**
* Re-validates the token before extending its cookie lifetime so that expired
* or revoked tokens cannot have their cookie window reopened by replay.
*/
export async function refreshSessionCookie(token) {
try {
if (!token || typeof token !== 'string' || token.trim() === '') {
return { success: false, error: 'Jeton de session invalide' };
}
const session = await validateSession(token);
if (!session) {
return { success: false, error: 'Session invalide ou expirée' };
}
const cookieStore = await cookies();
cookieStore.set(COOKIE_NAME, token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 30,
path: '/'
});
return { success: true };
} catch (error) {
fail(`Auth: refreshSessionCookie error: ${error.message}`);
return { success: false, error: 'Une erreur interne est survenue' };
}
}
export async function logoutAction() {
try {
const cookieStore = await cookies();
const token = cookieStore.get(COOKIE_NAME)?.value;
if (token) {
await deleteSession(token);
}
cookieStore.delete(COOKIE_NAME);
return { success: true, message: 'Déconnexion réussie' };
} catch (error) {
fail(`Auth: logoutAction error: ${error.message}`);
return { success: false, error: 'Une erreur interne est survenue. Veuillez réessayer.' };
}
}
export async function getSession() {
try {
const cookieStore = await cookies();
const token = cookieStore.get(COOKIE_NAME)?.value;
if (!token) return null;
const result = await validateSession(token);
if (result && result.sessionRefreshed) {
await refreshSessionCookie(token);
}
return result;
} catch (error) {
fail(`Auth: session validation error: ${error.message}`);
return null;
}
}
export async function forgotPasswordAction(formData) {
try {
const botCheck = validateAntiBotFields(formData);
if (!botCheck.valid) return { success: false, error: botCheck.error };
const ip = await getClientIp();
const rl = enforceRateLimit(ip, 'forgot_password');
if (rl && !rl.allowed) {
return { success: false, error: `Trop de tentatives. Réessayez dans ${formatRetryAfter(rl.retryAfterMs)}.` };
}
const email = formData.get('email');
const result = await requestPasswordReset(email);
if (result.token) {
await sendPasswordResetEmail(email, result.token, getPublicBaseUrl());
}
return {
success: true,
message: 'Si un compte existe avec cet e-mail, vous recevrez un lien pour réinitialiser votre mot de passe.'
};
} catch (error) {
fail(`Auth: forgotPasswordAction error: ${error.message}`);
return { success: false, error: 'Une erreur interne est survenue. Veuillez réessayer.' };
}
}
export async function resetPasswordAction(formData) {
try {
const ip = await getClientIp();
const rl = enforceRateLimit(ip, 'reset_password');
if (rl && !rl.allowed) {
return { success: false, error: `Trop de tentatives. Réessayez dans ${formatRetryAfter(rl.retryAfterMs)}.` };
}
const email = formData.get('email');
const token = formData.get('token');
const newPassword = formData.get('newPassword');
// Throw UserFacingError so the specific message reaches the client while
// unexpected system errors are sanitized in the catch below.
const isValid = await verifyResetToken(email, token);
if (!isValid) {
throw new UserFacingError('Jeton de réinitialisation invalide ou expiré');
}
await resetPassword({ email, token, newPassword });
return {
success: true,
message: 'Mot de passe réinitialisé avec succès. Vous pouvez maintenant vous connecter avec votre nouveau mot de passe.'
};
} catch (error) {
if (error instanceof UserFacingError) {
return { success: false, error: error.message };
}
fail(`Auth: resetPasswordAction error: ${error.message}`);
return { success: false, error: 'Une erreur interne est survenue. Veuillez réessayer.' };
}
}
export async function verifyEmailAction(formData) {
try {
const ip = await getClientIp();
const rl = enforceRateLimit(ip, 'verify_email');
if (rl && !rl.allowed) {
return { success: false, error: `Trop de tentatives. Réessayez dans ${formatRetryAfter(rl.retryAfterMs)}.` };
}
const email = formData.get('email');
const token = formData.get('token');
const isValid = await verifyEmailToken(email, token);
if (!isValid) {
throw new UserFacingError('Jeton de vérification invalide ou expiré');
}
const { findOne } = await import('../../core/database/crud.js');
const user = await findOne('zen_auth_users', { email });
if (!user) {
throw new UserFacingError('Utilisateur introuvable');
}
await verifyUserEmail(user.id);
return {
success: true,
message: 'E-mail vérifié avec succès. Vous pouvez maintenant vous connecter.'
};
} catch (error) {
if (error instanceof UserFacingError) {
return { success: false, error: error.message };
}
fail(`Auth: verifyEmailAction error: ${error.message}`);
return { success: false, error: 'Une erreur interne est survenue. Veuillez réessayer.' };
}
}
+1 -1
View File
@@ -8,7 +8,7 @@
*/
import { query, updateById } from '@zen/core/database';
import { updateUser } from './lib/auth.js';
import { updateUser } from './auth.js';
import { listRoles, getRoleById, createRole, updateRole, deleteRole, getUserRoles, assignUserRole, revokeUserRole } from '@zen/core/users';
import { uploadImage, deleteFile, generateUniqueFilename, getFileExtension, FILE_TYPE_PRESETS, FILE_SIZE_LIMITS, validateUpload } from '@zen/core/storage';
+4 -4
View File
@@ -5,16 +5,16 @@ import {
requestPasswordReset,
verifyUserEmail,
updateUser
} from '../../../core/users/auth.js';
} from '../../core/users/auth.js';
import { sendPasswordChangedEmail } from './email.js';
// Inject email sending into register (verification email) — kept here because
// it depends on JSX templates that live in features/auth.
// Inject sendPasswordChangedEmail — the JSX template lives in features/auth so
// the auth feature stays self-contained and core/users can remain pure server
// logic without JSX.
export function register(userData) {
return _register(userData);
}
// Inject sendPasswordChangedEmail — the template lives in features/auth.
export function resetPassword(resetData) {
return _resetPassword(resetData, { onPasswordChanged: sendPasswordChangedEmail });
}
+1 -38
View File
@@ -1,41 +1,4 @@
/**
* Auth Components Export
*
* Use these components to build custom auth pages for every flow (login, register, forgot,
* reset, confirm, logout) so they match your site's style.
* For a ready-made catch-all auth UI, use AuthPagesClient from '@zen/core/features/auth/pages'.
* For the default full-page auth (no custom layout), re-export from '@zen/core/features/auth/page'.
*
* --- Custom auth pages (all types) ---
*
* Pattern: server component loads session/searchParams and passes actions to a client wrapper;
* client wrapper uses useRouter for onNavigate and renders the Zen component.
*
* Component props:
* - LoginPage: onSubmit (loginAction), onSetSessionCookie, onNavigate, redirectAfterLogin, currentUser
* - RegisterPage: onSubmit (registerAction), onNavigate, currentUser
* - ForgotPasswordPage: onSubmit (forgotPasswordAction), onNavigate, currentUser
* - ResetPasswordPage: onSubmit (resetPasswordAction), onNavigate, email, token (from URL)
* - ConfirmEmailPage: onSubmit (verifyEmailAction), onNavigate, email, token (from URL)
* - LogoutPage: onLogout (logoutAction), onSetSessionCookie (optional)
*
* onNavigate receives 'login' | 'register' | 'forgot' | 'reset'. Map to your routes (e.g. /auth/${page}).
* For reset/confirm, pass email and token from searchParams. Full guide: see README-custom-login.md in this package.
* Protect routes with protect() from '@zen/core/features/auth', redirectTo your login path.
*
* --- Dashboard / user display ---
*
* UserAvatar, UserMenu, AccountSection, useCurrentUser: see README-dashboard.md.
*/
export { default as AuthPagesLayout } from './AuthPagesLayout.js';
export { default as AuthPagesClient } from './AuthPages.js';
export { default as LoginPage } from './pages/LoginPage.js';
export { default as RegisterPage } from './pages/RegisterPage.js';
export { default as ForgotPasswordPage } from './pages/ForgotPasswordPage.js';
export { default as ResetPasswordPage } from './pages/ResetPasswordPage.js';
export { default as ConfirmEmailPage } from './pages/ConfirmEmailPage.js';
export { default as LogoutPage } from './pages/LogoutPage.js';
'use client';
export { default as UserAvatar } from './UserAvatar.js';
export { default as UserMenu } from './UserMenu.js';
+4 -4
View File
@@ -1,12 +1,12 @@
import { render } from '@react-email/components';
import { fail, info } from '@zen/core/shared/logger';
import { sendEmail } from '@zen/core/email';
import { VerificationEmail } from '../templates/VerificationEmail.jsx';
import { PasswordResetEmail } from '../templates/PasswordResetEmail.jsx';
import { PasswordChangedEmail } from '../templates/PasswordChangedEmail.jsx';
import { VerificationEmail } from './templates/VerificationEmail.jsx';
import { PasswordResetEmail } from './templates/PasswordResetEmail.jsx';
import { PasswordChangedEmail } from './templates/PasswordChangedEmail.jsx';
export { createEmailVerification, verifyEmailToken, createPasswordReset, verifyResetToken, deleteResetToken }
from '../../../core/users/verifications.js';
from '../../core/users/verifications.js';
async function sendVerificationEmail(email, token, baseUrl) {
const appName = process.env.ZEN_NAME || 'ZEN';
+9 -21
View File
@@ -1,11 +1,9 @@
/**
* Zen Authentication Module - Server-side utilities
*
* For client components, use '@zen/core/auth/pages'
* For server actions, use '@zen/core/auth/actions'
* Zen Authentication — server barrel.
* Client components live in @zen/core/features/auth/components.
* Server actions live in @zen/core/features/auth/actions.
*/
// Authentication library (server-side only)
export {
register,
login,
@@ -13,18 +11,16 @@ export {
resetPassword,
verifyUserEmail,
updateUser
} from './lib/auth.js';
} from './auth.js';
// Session management (server-side only)
export {
createSession,
validateSession,
deleteSession,
deleteUserSessions,
refreshSession
} from './lib/session.js';
} from './session.js';
// Email utilities (server-side only)
export {
createEmailVerification,
verifyEmailToken,
@@ -34,24 +30,17 @@ export {
sendVerificationEmail,
sendPasswordResetEmail,
sendPasswordChangedEmail
} from './lib/email.js';
} from './email.js';
// Password utilities (server-side only)
export {
hashPassword,
verifyPassword,
generateToken,
generateId
} from './lib/password.js';
} from './password.js';
// Middleware (server-side only)
export {
protect,
checkAuth,
requireRole
} from './middleware/protect.js';
export { protect, checkAuth, requireRole } from './protect.js';
// Server Actions (server-side only)
export {
registerAction,
loginAction,
@@ -62,5 +51,4 @@ export {
verifyEmailAction,
setSessionCookie,
refreshSessionCookie
} from './actions/authActions.js';
} from './actions.js';
+1 -1
View File
@@ -1 +1 @@
export { hashPassword, verifyPassword, generateToken, generateId } from '../../../core/users/password.js';
export { hashPassword, verifyPassword, generateToken, generateId } from '../../core/users/password.js';
+8 -72
View File
@@ -1,83 +1,19 @@
/**
* Route Protection Middleware
* Utilities to protect routes and check authentication
*/
import { getSession } from '../actions/authActions.js';
import { getSession } from './actions.js';
import { redirect } from 'next/navigation';
/**
* Protect a page - requires authentication
* Use this in server components to require authentication
*
* @param {Object} options - Protection options
* @param {string} options.redirectTo - Where to redirect if not authenticated (default: '/auth/login')
* @returns {Promise<Object>} Session object with user data
*
* @example
* // In a server component:
* import { protect } from '@zen/core/features/auth';
*
* export default async function ProtectedPage() {
* const session = await protect();
* return <div>Welcome, {session.user.name}!</div>;
* }
*/
async function protect(options = {}) {
const { redirectTo = '/auth/login' } = options;
export async function protect({ redirectTo = '/auth/login' } = {}) {
const session = await getSession();
if (!session) {
redirect(redirectTo);
}
if (!session) redirect(redirectTo);
return session;
}
/**
* Check if user is authenticated
* Use this when you want to check authentication without forcing a redirect
*
* @returns {Promise<Object|null>} Session object or null if not authenticated
*
* @example
* import { checkAuth } from '@zen/core/features/auth';
*
* export default async function Page() {
* const session = await checkAuth();
* return session ? <div>Logged in</div> : <div>Not logged in</div>;
* }
*/
async function checkAuth() {
return await getSession();
export async function checkAuth() {
return getSession();
}
/**
* Require a specific role
* @param {Array<string>} allowedRoles - Array of allowed roles
* @param {Object} options - Options
* @returns {Promise<Object>} Session object
*/
async function requireRole(allowedRoles = [], options = {}) {
const { redirectTo = '/auth/login', forbiddenRedirect = '/' } = options;
export async function requireRole(allowedRoles = [], { redirectTo = '/auth/login', forbiddenRedirect = '/' } = {}) {
const session = await getSession();
if (!session) {
redirect(redirectTo);
}
if (!allowedRoles.includes(session.user.role)) {
redirect(forbiddenRedirect);
}
if (!session) redirect(redirectTo);
if (!allowedRoles.includes(session.user.role)) redirect(forbiddenRedirect);
return session;
}
export {
protect,
checkAuth,
requireRole
};
+1 -1
View File
@@ -1 +1 @@
export { createSession, validateSession, deleteSession, deleteUserSessions, refreshSession } from '../../../core/users/session.js';
export { createSession, validateSession, deleteSession, deleteUserSessions, refreshSession } from '../../core/users/session.js';