@ratio1/cstore-auth-ts
v0.6.1
Published
Plug-and-play authentication layer for Ratio1 CStore hashes. This TypeScript library wraps the official [@ratio1/ratio1-sdk-ts](https://github.com/Ratio1/ratio1-sdk-ts) SDK, providing a minimal API to bootstrap an admin account and manage simple username/
Readme
@ratio1/cstore-auth-ts
Plug-and-play authentication layer for Ratio1 CStore hashes. This TypeScript library wraps the official @ratio1/ratio1-sdk-ts SDK, providing a minimal API to bootstrap an admin account and manage simple username/password credentials.
Features
- Hash-backed user store with Argon2id hashing and automatic Node.js scrypt fallback
- Strict username validation and canonicalisation (lowercase keys)
- Admin bootstrap workflow driven by environment variables
- Strong typing, unit tests (Vitest), linting, and dual ESM/CJS builds via tsup
- Works in Node.js services and Next.js API routes
Installation
pnpm add @ratio1/cstore-auth-ts
# or
npm install @ratio1/cstore-auth-tsRequired environment variables
| Variable | Description |
| -------------------------------------- | -------------------------------------------------------------------------------------------------- |
| R1EN_CSTORE_AUTH_HKEY | Hash key that stores all user records (e.g. auth:default). |
| R1EN_CSTORE_AUTH_SECRET | Long-lived server-side pepper mixed into password hashes. |
| R1EN_CSTORE_AUTH_BOOTSTRAP_ADMIN_PWD | One-time bootstrap password for the initial admin user. Required until the admin account exists. |
Note: Legacy environment variable names (
EE_CSTORE_AUTH_HKEY,EE_CSTORE_AUTH_SECRET,EE_CSTORE_AUTH_BOOTSTRAP_ADMIN_PW) are still supported for backwards compatibility but are deprecated.
Quick start
import { CStoreAuth } from '@ratio1/cstore-auth-ts';
const auth = new CStoreAuth();
await auth.simple.init();
await auth.simple.createUser('alice', 'S3curePassw0rd', {
metadata: { email: '[email protected]' }
});
const user = await auth.simple.authenticate('alice', 'S3curePassw0rd');
console.log(user);
// → { username: 'alice', role: 'user', metadata: { email: '[email protected]' }, createdAt: '...', updatedAt: '...', type: 'simple' }
// Retrieve all users
const allUsers = await auth.simple.getAllUsers();
console.log(`Total users: ${allUsers.length}`);
allUsers.forEach((u) => console.log(`- ${u.username} (${u.role})`));
// Update user metadata or role
// IMPORTANT: Implement authorization checks in your application layer!
// Example: Only allow users to edit themselves or admins to edit anyone
await auth.simple.updateUser('alice', {
metadata: { email: '[email protected]', verified: true }
});
// Change password (requires current password)
await auth.simple.changePassword('alice', 'S3curePassw0rd', 'NewP@ssw0rd!');Public API
interface CStoreAuthOptions {
hkey?: string;
secret?: string;
client?: CStoreLikeClient;
hasher?: PasswordHasher;
now?: () => Date;
logger?: Pick<Console, 'debug' | 'info' | 'warn' | 'error'>;
}
class CStoreAuth {
constructor(opts?: CStoreAuthOptions);
simple: {
init(): Promise<void>;
createUser<TMeta = Record<string, unknown>>(
username: string,
password: string,
opts?: CreateUserOptions<TMeta>
): Promise<PublicUser<TMeta>>;
authenticate<TMeta = Record<string, unknown>>(
username: string,
password: string
): Promise<PublicUser<TMeta>>;
getUser<TMeta = Record<string, unknown>>(username: string): Promise<PublicUser<TMeta> | null>;
getAllUsers<TMeta = Record<string, unknown>>(): Promise<PublicUser<TMeta>[]>;
updateUser<TMeta = Record<string, unknown>>(
username: string,
opts: UpdateUserOptions<TMeta>
): Promise<PublicUser<TMeta>>;
changePassword(username: string, currentPassword: string, newPassword: string): Promise<void>;
};
}Errors are surfaced as descriptive subclasses (EnvVarMissingError, AuthInitError, InvalidUsernameError, InvalidCredentialsError, UserExistsError, UserNotFoundError, etc.).
Authorization Patterns
⚠️ IMPORTANT: updateUser does not enforce authorization. You must implement authorization checks in your application layer.
// Example: Users can edit themselves, admins can edit anyone
async function updateUserWithAuth(
currentUser: PublicUser,
targetUsername: string,
updates: UpdateUserOptions
) {
// Check if user is editing themselves OR is an admin
const isEditingSelf = currentUser.username === targetUsername;
const isAdmin = currentUser.role === 'admin';
if (!isEditingSelf && !isAdmin) {
throw new Error('Unauthorized: You can only edit your own profile');
}
// Only admins can change roles
if (updates.role && !isAdmin) {
throw new Error('Unauthorized: Only admins can change roles');
}
return await auth.simple.updateUser(targetUsername, updates);
}
// Example: Express.js middleware
app.put('/api/users/:username', async (req, res) => {
const currentUser = req.session.user; // From authenticated session
const { username } = req.params;
const updates = req.body;
try {
// Authorization check
if (currentUser.username !== username && currentUser.role !== 'admin') {
return res.status(403).json({ error: 'Forbidden' });
}
// Only admins can change roles
if (updates.role && currentUser.role !== 'admin') {
return res.status(403).json({ error: 'Only admins can change roles' });
}
const updated = await auth.simple.updateUser(username, updates);
res.json(updated);
} catch (error) {
res.status(400).json({ error: error.message });
}
});Security notes
Password Security
- Argon2id (via
@node-rs/argon2) is used when available. The module automatically downgrades to Node's built-incrypto.scryptwith strong defaults when Argon2 cannot load. - Each user receives a random 16-byte salt and a global pepper (
R1EN_CSTORE_AUTH_SECRET). - Password hashes are stored versioned to enable future migrations.
changePasswordalways requires current password verification.- Secrets and hash material never leave the module through logs.
Authorization
- ⚠️
updateUserdoes NOT enforce authorization - implement checks in your application layer - Recommended pattern: Users can edit themselves, admins can edit anyone
- Role changes should be restricted to admins only
- Consider field-level permissions (e.g., users can't set
verified: trueon themselves)
Data Validation
- Usernames are canonicalised to lowercase and must match
[a-z0-9._-]{3,64}. - Validation happens on every entry point.
- Only metadata is returned publicly (passwords never exposed).
Development
pnpm install
pnpm run lint
pnpm test # Run unit tests
pnpm run test:integration # Run integration tests
pnpm run test:all # Run all tests
pnpm run build
pnpm run docsTesting
The project includes two test suites:
- Unit tests (
test/**/*.spec.ts): Fast, isolated tests with mocked dependencies - Integration tests (
test/integration/**/*.integration.spec.ts): Complete workflow tests with realistic cleanup patterns
Integration tests follow best practices including:
- Cleanup before and after each test (setting values to null before deletion)
- Isolated test environments with unique hash keys
- Serial execution to avoid race conditions
- Extended timeouts for password hashing operations
See test/integration/README.md for detailed integration testing documentation.
Typedoc emits HTML documentation to docs/. GitHub Actions (see .github/workflows/ci.yml) runs linting, type-checking, tests, build, and docs generation on Node.js 18 and 20.
Roadmap
- Additional providers (Google, OAuth 2.0) alongside the current
simplemethod - Session issuance (JWT/cookies) and rate limiting/lockouts
- Password rotation and secret (pepper) rotation workflows
- Multi-tenant or namespaced user separation
