Credentials and Sessions
Signup, signin, signout, current-user lookup, password reset, email verification, and invitations
For passwordless email login, see Magic Links.
Credentials config
Configure credentials by passing options to kyzenAuthMiddleware({...}). The runtime helpers read those options on first request:
// src/middleware.ts
import { kyzenAuthMiddleware } from '@kuratchi/kyzen/middleware';
export default defineMiddleware({
auth: kyzenAuthMiddleware({
cookieName: 'kuratchi_session',
sessionEnabled: true,
credentials: {
binding: 'DB', // D1 binding name
defaultRole: 'member',
sessionDuration: 30 * 24 * 60 * 60 * 1000,
minPasswordLength: 8,
signUpRedirect: '/auth/verify-email',
signInRedirect: '/',
signOutRedirect: '/auth/signin',
// Email verification (optional — enables the verify flow)
requireEmailVerified: true,
sendVerificationEmail: async ({ email, token }) => {
// your transactional sender (Resend, SendGrid, AWS SES, env.EMAIL, etc.)
},
sendWelcomeEmail: async ({ email }) => { /* ... */ },
// Invitations (org-mode only)
sendInviteEmail: async ({ email, token, invitedBy }) => { /* ... */ },
},
}),
});
Then call the helpers anywhere request code runs:
import {
signUp,
signIn,
signOut,
getCurrentUser,
requestPasswordReset,
resetPassword,
verifyEmail,
resendEmailVerification,
inviteMember,
acceptInvite,
} from '@kuratchi/kyzen';
Supported credentials options
| Option | Type | Default | Purpose |
|---|---|---|---|
binding |
string |
'DB' |
D1 binding name for the admin database |
defaultRole |
string |
'user' |
Role assigned at signup |
sessionDuration |
number |
30 * 24 * 60 * 60 * 1000 |
Session lifetime in ms |
minPasswordLength |
number |
8 |
Minimum password length |
signUpRedirect |
string |
'/auth/login' |
Redirect after successful signup |
signInRedirect |
string |
'/admin' |
Redirect after successful signin |
signOutRedirect |
string |
'/auth/login' |
Redirect after signout |
excludeFields |
string[] |
['passwordHash'] |
Fields stripped from getCurrentUser() |
requireEmailVerified |
boolean |
false |
Reject signin when users.emailVerified is null |
unverifiedMessage |
string |
'Please verify your email...' |
Error returned when requireEmailVerified blocks signin |
tokenExpiryMs |
number |
24 * 60 * 60 * 1000 |
Lifetime of verification + invite tokens |
sendVerificationEmail |
(params) => Promise<void> |
— | App-supplied callback that delivers a verification email after signUp |
sendWelcomeEmail |
(params) => Promise<void> |
— | Optional welcome email after verifyEmail succeeds |
sendInviteEmail |
(params) => Promise<void> |
— | Required for inviteMember (org-mode invitations) |
Signup
signUp(formData) expects:
email(required)password(required)name(optional)organizationName(required when organizations are configured — see Organizations)
The helper:
- Validates password length (rejects if shorter than
minPasswordLength). - Hashes the password with
AUTH_SECRETas a pepper. - Creates the user (in the org's Durable Object when org-mode is active, in admin D1 otherwise).
- If
sendVerificationEmailis configured: issues a verification token, calls the callback, redirects tosignUpRedirect(default/auth/verify-email) without auto-login. - If not configured: auto-creates a session and redirects to
signUpRedirect(default/). - Logs
user.signupactivity.
Signin
signIn(formData) expects:
emailpassword
On success it:
- Looks up the user (via
organizationUsersemail→org index in org-mode). - Verifies the password hash.
- If
requireEmailVerifiedis true andemailVerifiedis null → throwsunverifiedMessage. - Creates a session token + record (in the org DO or admin DB).
- Builds a signed session cookie carrying the DO routing key.
- Sets the cookie and redirects to
signInRedirect. - Logs
user.loginactivity.
Signout
signOut() resolves the current session from the cookie, deletes the session record (DO RPC in org-mode), clears the cookie, and redirects.
Current user lookup
getCurrentUser() reads the signed session cookie, validates the backing session, and returns a safe user object:
{
id: string,
email: string,
name?: string | null,
role: string,
emailVerified?: number | null,
// org-mode adds these:
organizationId: string, // the actual org UUID — use this for FKs
organizationName?: string, // human label for chrome
// ...other user columns
}
passwordHash is excluded by default. Override with excludeFields.
Password reset
import { requestPasswordReset, resetPassword } from '@kuratchi/kyzen';
requestPasswordReset({ formData }) reads formData.email, generates a reset token, stores its hash in passwordResets, and (if you've wired it) hands the plaintext token to your email-sender. resetPassword({ formData }) reads token + password, verifies the token, updates the password hash, and invalidates active sessions.
Email verification
When sendVerificationEmail is configured, signUp issues a token and your callback delivers the email. The user clicks the link → your route calls verifyEmail(token):
// src/routes/auth/verify-email/index.koze
<script>
import { verifyEmail, resendEmailVerification } from '@kuratchi/kyzen';
import { searchParams } from '@kuratchi/koze/request';
const token = searchParams.get('token');
const email = searchParams.get('email');
let verified = false;
let error: string | null = null;
if (token) {
const result = await verifyEmail(token);
verified = result.success;
error = result.error || null;
}
</script>
if (verified) {
<p>Email verified. <a href="/auth/signin">Sign in</a>.</p>
} else if (error) {
<p>{error}</p>
<form action={resendEmailVerification} method="POST">
<input type="hidden" name="email" value={email} />
<button type="submit">Resend verification email</button>
</form>
}
verifyEmail(token) returns { success: boolean; error?: string } — never throws. resendEmailVerification({ formData }) is a form-action; it reads formData.email and re-issues a token. Both flows automatically route through the org DO when org-mode is active.
If sendWelcomeEmail is configured, it fires after a successful verifyEmail.
Invitations
See Organizations and schema — invitations are org-mode-only.
Cookie model
The session cookie is AES-GCM-encrypted with AUTH_SECRET and contains { doName, tokenHash }:
doNameis the per-org DO routing key in org-mode, or'default'in single-tenant mode.tokenHashis the hash of the session token; the plaintext token never leaves the cookie.
Session persistence uses the sessions table (admin DB or per-org DO), not the cookie itself — invalidating a session deletes the row, which is checked on every getCurrentUser() call.