@drop-in/pass
v0.5.0
Published
Your drop-in season pass. aka Auth
Downloads
77
Readme
@drop-in/pass
Your drop-in season pass. aka Auth
A secure, modern authentication library for SvelteKit applications with HttpOnly JWT cookies, refresh token rotation, and comprehensive session management. Runtime agnostic - works in Node.js, Cloudflare Workers, Deno, Bun, and other environments.
✨ Features
- 🔒 Secure by default - HttpOnly cookies, CSRF protection, bcrypt password hashing
- 🔄 Automatic token refresh - Transparent JWT renewal with refresh token rotation
- 📧 Email verification - Built-in email verification workflow with flexible provider configuration
- 🌐 Runtime agnostic - Works in Node.js, Cloudflare Workers, Deno, Bun, and other environments
- 🏗️ SvelteKit optimized - Native hooks integration and SSR support
- 📊 Session management - Server-side user context and authentication state
- 🧪 Well tested - 86+ tests covering all core functionality
- 📝 TypeScript first - Full type safety throughout
🚀 Quick Start
Installation
npm install @drop-in/passDatabase Setup
Database is provided via dependency injection only.
- Create a Drizzle instance in your app.
- Pass it to our SvelteKit handle factories.
# .env
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
JWT_SECRET="your-secret-key-here"Database is provided via dependency injection only; pass your Drizzle instance to create_session_handle(db) and create_pass_routes(db) as shown below.
Injecting your Drizzle instance
Use our factories in hooks.server.ts (or equivalent) to inject your Drizzle instance before requests hit auth routes.
Example with Node Postgres Pool (Node runtimes):
// src/hooks.server.ts (or your server init)
import { create_pass_routes, create_session_handle } from '@drop-in/pass';
import { drizzle } from 'drizzle-orm/node-postgres';
import pg from 'pg';
import * as schema from '@drop-in/pass/schema';
import { sequence } from '@sveltejs/kit/hooks';
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const drizzleDb = drizzle(pool, { schema });
export const handle = sequence(
create_session_handle(drizzleDb),
create_pass_routes(drizzleDb)
);Example with Cloudflare Hyperdrive (Workers):
// src/hooks.server.ts (Cloudflare Workers)
import { create_pass_routes, create_session_handle } from '@drop-in/pass';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from '@drop-in/pass/schema';
import { sequence } from '@sveltejs/kit/hooks';
export const handle: Handle = async ({ event, resolve }) => {
const env = event.platform?.env as any;
const sql = postgres(env.DATABASE_URL, { prepare: true }); // via Hyperdrive
const db = drizzle(sql, { schema });
const chain = sequence(
create_session_handle(db),
create_pass_routes(db)
);
return chain({ event, resolve });
};Example with Neon (serverless):
// src/hooks.server.ts (Neon serverless)
import { create_pass_routes, create_session_handle } from '@drop-in/pass';
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
import * as schema from '@drop-in/pass/schema';
import { sequence } from '@sveltejs/kit/hooks';
const sql = neon(process.env.DATABASE_URL!);
const db = drizzle(sql, { schema });
export const handle = sequence(
create_session_handle(db),
create_pass_routes(db)
);Notes:
- Instantiate Drizzle per process/request-lifetime depending on your runtime model.
- In development, you can enable debug logs with
DEBUGorNODE_ENV !== 'production'. - All server APIs accept a
dbinstance via factory functions; you control how and where Drizzle is instantiated.
Email Configuration
Password reset links are generated using create_password_link(email) and include query params: email, key (token), and expire (timestamp). The default expiration is 24 hours. The reset endpoint expects these parameters.
Configure your email provider in drop-in.config.js:
// For Cloudflare Workers with Resend
const sendEmail = async ({ to, subject, html, from }) => {
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ to, subject, html, from }),
});
if (!response.ok) {
throw new Error(`Failed to send email: ${response.statusText}`);
}
};
export default {
email: {
from: '[email protected]',
sendEmail,
},
app: {
url: 'https://yourdomain.com',
name: 'Your App',
route: '/dashboard'
}
};Supported email providers:
- Resend - Modern email API, perfect for Cloudflare Workers
- MailChannels - Free email sending for Cloudflare Workers
- SendGrid - Reliable email delivery service
- SMTP - Traditional email with
@drop-in/beeperfor Node.js
See Email Configuration Guide for detailed examples.
Basic Setup
Note: Signing up (POST /api/auth/register) automatically triggers a verification email in the background. The response is not delayed by email sending; failures are logged and do not block signup.
- Configure your hooks (
src/hooks.server.ts):
import { create_pass_routes, create_session_handle } from '@drop-in/pass';
import { sequence } from '@sveltejs/kit/hooks';
import { drizzle } from 'drizzle-orm/node-postgres';
import pg from 'pg';
import * as schema from '@drop-in/pass/schema';
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const db = drizzle(pool, { schema });
export const handle = sequence(
create_session_handle(db), // Populates event.locals.user automatically
create_pass_routes(db) // Handles auth routes (/api/auth/*)
);- Configure global settings (
drop-in.config.js):
export default {
email: {
from: '[email protected]',
sendEmail: yourEmailFunction, // Your email implementation
},
app: {
url: 'https://yourdomain.com',
name: 'Your App Name',
route: '/dashboard'
}
};Usage
Client-Side Authentication
import { pass } from '@drop-in/pass/client';
// Sign up
try {
const result = await pass.signup('[email protected]', 'securepassword');
console.log('Signed up successfully!', result.user);
} catch (error) {
console.error('Signup failed:', error.message);
}
// Login
try {
const result = await pass.login('[email protected]', 'securepassword');
console.log('Logged in successfully!', result.user);
} catch (error) {
console.error('Login failed:', error.message);
}
// Get current user
try {
const { user } = await pass.me();
console.log('Current user:', user);
} catch (error) {
console.log('Not authenticated');
}
// Logout
await pass.logout();Server-Side Usage
// In load functions, API routes, or hooks
export async function load({ locals }) {
if (locals.user) {
console.log('User is authenticated:', locals.user.id);
return {
user: locals.user
};
}
// User is not authenticated
return {};
}// Manual authentication in API routes
import { authenticate_user } from '@drop-in/pass';
export async function GET({ cookies }) {
const auth = await authenticate_user(db, cookies);
if (!auth) {
return new Response('Unauthorized', { status: 401 });
}
// User is authenticated
console.log('User ID:', auth.user_id);
return new Response('Hello authenticated user!');
}🛡️ Security Features
HttpOnly Cookies
- Access tokens are HttpOnly, secure, and long-lived (90 days)
- Refresh tokens are HttpOnly, secure, and long-lived (90 days)
- SameSite=strict protection against CSRF attacks
- Automatic token refresh happens transparently
Password Security
- bcrypt hashing with salt rounds (configurable, default: 10)
- Backward compatibility for password hash migration
- Minimum password requirements (6+ characters, configurable)
Session Management
- Database-stored refresh tokens with automatic cleanup
- Token rotation on each refresh
- Secure logout that invalidates all tokens
- Protection against token reuse
📖 API Reference
Client API (@drop-in/pass/client)
pass.signup(email: string, password: string)
Creates a new user account.
Returns: Promise<{ user: User }>
Throws: Error with validation or server error messages
pass.login(email: string, password: string)
Authenticates a user.
Returns: Promise<{ user: User }>
Throws: Error with authentication failure details
pass.logout()
Logs out the current user.
Returns: Promise<Response>
pass.requestPasswordReset(email: string)
Requests a password reset email. Always returns success to avoid user enumeration.
Returns: Promise<Response>
pass.resetPassword(email: string, token: string, expire: number, password: string)
Completes password reset. On success, sets HttpOnly cookies for JWT and refresh token.
Returns: Promise<Response>
pass.me()
Gets current authenticated user information.
Returns: Promise<{ user: User }>
Throws: Error if not authenticated
Server API
authenticate_user(db: DrizzleDb, cookies: Cookies)
Manually authenticate a user from cookies.
const auth = await authenticate_user(db, cookies);
if (auth) {
console.log('User ID:', auth.user_id);
}populate_user_session(db: DrizzleDb, event: RequestEvent)
Manually populate event.locals.user with authenticated user data.
await populate_user_session(db, event);
console.log(event.locals.user); // User object or undefinedBuilt-in Routes
The library automatically handles these routes when using create_pass_routes(db):
POST /api/auth/login- User loginPOST /api/auth/register- User registration (auto-sends verification email; non-blocking)POST /api/auth/logout- User logoutGET /api/auth/me- Get current userPOST /api/auth/verify-email- Email verificationPOST /api/auth/send-verify-email- Send verification emailPOST /api/auth/forgot-password- Request password reset (always returns success)POST /api/auth/reset-password- Complete password reset and sign in
🔧 Configuration
Cookie Settings
// Refresh token settings (src/cookies.ts)
export const cookie_options = {
httpOnly: true,
secure: true,
path: '/',
sameSite: 'strict' as const,
maxAge: 60 * 60 * 24 * 90, // 90 days
};
// JWT settings
export const jwt_cookie_options = {
path: '/',
maxAge: 60 * 60 * 24 * 90, // 90 days
httpOnly: true,
sameSite: 'strict' as const,
secure: true,
};Environment Variables
# Required
DATABASE_URL="postgresql://..."
JWT_SECRET="your-jwt-secret"
# Optional email API keys (choose one based on your provider)
RESEND_API_KEY="re_your_api_key" # For Resend
SENDGRID_API_KEY="SG.your_api_key" # For SendGrid
# MailChannels requires no API key for Cloudflare Workers
# Legacy SMTP settings (if using @drop-in/beeper)
EMAIL_HOST="smtp.gmail.com"
EMAIL_PORT="587"
EMAIL_SECURE="true"
EMAIL_USER="[email protected]"
EMAIL_PASSWORD="your-app-password"🧪 Testing
The library includes comprehensive test coverage:
npm test # Run all tests
npm run test:watch # Watch modeTest Coverage:
- ✅ Password hashing and verification
- ✅ JWT creation and validation
- ✅ Token refresh flow
- ✅ Authentication middleware
- ✅ Login/signup flows
- ✅ Utility functions
- ✅ Client API calls
🔄 Migration from Non-HttpOnly Setup
If you're upgrading from a version that used readable JWTs:
- Update hooks.server.ts to include
create_session_handle(db) - Replace client-side JWT reading with server-side
locals.userorpass.me() - Remove manual cookie handling - all cookie management is now automatic
See SECURITY-UPGRADE.md for detailed migration instructions.
🤝 TypeScript Support
Full TypeScript support with type definitions for:
import type { User } from '@drop-in/pass/schema';
// Event locals typing is automatic
declare global {
namespace App {
interface Locals {
user?: Partial<User>;
}
}
}🚨 Security Considerations
- Always use HTTPS in production - cookies won't work properly over HTTP
- Set secure environment variables - never commit secrets to version control
- Configure CSP headers - additional XSS protection
- Monitor for suspicious activity - implement rate limiting for auth endpoints
- Regular security updates - keep dependencies updated
📋 Development
# Install dependencies
npm install
# Run tests
npm test
# Build the package
npm run build
# Development mode
npm run dev🐛 Troubleshooting
Common Issues
"Not authenticated" errors in production
- Ensure HTTPS is properly configured
- Check that cookies are being set with correct domain
- Verify SameSite settings for your deployment
Database connection errors
- Verify
DATABASE_URLenvironment variable - Ensure PostgreSQL is running and accessible
- Check database schema is properly set up
Email verification not working or no email received
- Configure email provider in
drop-in.config.jswith yoursendEmailcallback (signup triggers verification automatically) - Set up email service credentials (API keys) in environment variables
- Check spam/junk folders
- Verify your email provider configuration is correct
Runtime compatibility issues
- Ensure your email implementation uses only Web APIs (fetch, etc.) for Cloudflare Workers
- For Node.js SMTP, use
@drop-in/beeperas your email callback - Avoid Node.js-specific modules in Cloudflare Workers environments
Debug Mode
Enable debug logging:
DEBUG=drop-in:* npm run dev🤝 Contributing
We welcome contributions! Please see our contributing guidelines and:
- Add tests for any new features
- Update documentation for API changes
- Follow TypeScript best practices
- Ensure security review for auth-related changes
📄 License
ISC License - see LICENSE file for details.
🙏 Acknowledgments
Built with:
- bcryptjs - Password hashing
- jose - JWT handling
- drizzle-orm - Database ORM
- nanoid - ID generation
Made with ❤️ for the SvelteKit community
