Credentials and Sessions | Kyzen | Primitives Docs

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:

  1. Validates password length (rejects if shorter than minPasswordLength).
  2. Hashes the password with AUTH_SECRET as a pepper.
  3. Creates the user (in the org's Durable Object when org-mode is active, in admin D1 otherwise).
  4. If sendVerificationEmail is configured: issues a verification token, calls the callback, redirects to signUpRedirect (default /auth/verify-email) without auto-login.
  5. If not configured: auto-creates a session and redirects to signUpRedirect (default /).
  6. Logs user.signup activity.

Signin

signIn(formData) expects:

  • email
  • password

On success it:

  1. Looks up the user (via organizationUsers email→org index in org-mode).
  2. Verifies the password hash.
  3. If requireEmailVerified is true and emailVerified is null → throws unverifiedMessage.
  4. Creates a session token + record (in the org DO or admin DB).
  5. Builds a signed session cookie carrying the DO routing key.
  6. Sets the cookie and redirects to signInRedirect.
  7. Logs user.login activity.

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.

The session cookie is AES-GCM-encrypted with AUTH_SECRET and contains { doName, tokenHash }:

  • doName is the per-org DO routing key in org-mode, or 'default' in single-tenant mode.
  • tokenHash is 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.