# Credentials and Sessions

> Signup, signin, signout, current-user lookup, password reset, email verification, and invitations

Package: Kyzen
Canonical: https://kuratchi.dev/docs/kyzen/credentials-and-sessions
Markdown: https://kuratchi.dev/docs/kyzen/credentials-and-sessions.md

For passwordless email login, see [Magic Links](/docs/kyzen/magic-links).

## Credentials config

Configure credentials by passing options to `kyzenAuthMiddleware({...})`. The runtime helpers read those options on first request:

```ts
// 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:

```ts
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](/docs/kyzen/organizations-and-schema))

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:

```ts
{
  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

```ts
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)`:

```ts
// 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](/docs/kyzen/organizations-and-schema#invitations) — invitations are org-mode-only.

## Cookie model

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.
