sentri
sentri
Auth and authorization library for Node.js — Express, Fastify, Hono, Elysia, and Koa. Supports two modes:
- Server mode — runs as a standalone auth server with its own database schema (Kysely), issues JWTs, and exposes auth endpoints including a public key endpoint for SSO.
- Client mode — used by other apps to validate tokens issued by the auth server, without a database.
Subpath exports
| Import | Description |
|---|---|
sentri |
Full package re-export (backward compat) |
sentri/core |
Framework-agnostic types, SentriError, SentriLogger |
sentri/express |
Express adapter — createAuthExpress, middleware, router |
sentri/fastify |
Fastify adapter — createAuthFastify, preHandlers, plugin |
sentri/hono |
Hono adapter — createAuthHono, middleware, router |
sentri/elysia |
Elysia adapter — createAuthElysia, middleware, router |
sentri/koa |
Koa adapter — createAuthKoa, middleware, router |
Table of Contents
- Installation
- Quick Start
- Server Mode
- Client Mode
- SSO Flow
- Multi-Identifier
- Configuration
- Endpoints
- Middleware
- Token Utilities
- Error Handling
- Request Idempotency
- Logger
- Migration Guide
Installation
npm install sentrikysely is bundled with sentri — no separate installation needed for Kysely. However, you must install the driver for your database of choice.
For PostgreSQL:
npm install pgFor other databases:
# MySQL
npm install mysql2
# SQLite
npm install better-sqlite3Peer dependencies (install only what you use):
# Express
npm install express
# Fastify
npm install fastify @fastify/cookie
# Hono
npm install hono
# Elysia
npm install elysiaQuick Start
Express (server mode, recommended)
import express from "express";
import { createAuthExpress } from "sentri/express";
import { PostgresDialect } from "kysely";
import pg from "pg";
const { Pool } = pg;
const auth = createAuthExpress({
mode: "server",
validRoles: ["user", "admin"] as const,
dialect: new PostgresDialect({
pool: new Pool({ connectionString: process.env.DATABASE_URL! }),
}),
});
const app = express();
app.use(express.json());
app.use(auth.idempotencyMiddleware());
await auth.migrate();
app.use("/auth", auth.router());
app.get("/me", auth.protect(), (req, res) => res.json(req.user));
app.use(auth.errorHandler());
app.listen(3000);createAuthServer() generates an RSA-2048 key pair at startup and enables GET /keys automatically.
For detailed Express docs see src/adapters/express/README.md.
Fastify (server mode)
import Fastify from "fastify";
import cookie from "@fastify/cookie";
import { createAuthFastify } from "sentri/fastify";
import { PostgresDialect } from "kysely";
import pg from "pg";
const { Pool } = pg;
const auth = createAuthFastify({
mode: "server",
validRoles: ["user", "admin"] as const,
dialect: new PostgresDialect({
pool: new Pool({ connectionString: process.env.DATABASE_URL! }),
}),
});
await auth.migrate();
const app = Fastify();
await app.register(cookie);
await app.register(auth.plugin(), { prefix: "/auth" });
app.setErrorHandler(auth.errorHandler());
app.get("/me", { preHandler: auth.protect() }, async (request, reply) => {
reply.send(request.user);
});
await app.listen({ port: 3000 });For detailed Fastify docs see src/adapters/fastify/README.md.
Hono (server mode)
import { Hono } from "hono";
import { createAuthHono } from "sentri/hono";
import type { SentriHonoEnv } from "sentri/hono";
import { PostgresDialect } from "kysely";
import pg from "pg";
const { Pool } = pg;
const auth = createAuthHono({
mode: "server",
validRoles: ["user", "admin"] as const,
dialect: new PostgresDialect({
pool: new Pool({ connectionString: process.env.DATABASE_URL! }),
}),
});
await auth.migrate();
const app = new Hono<SentriHonoEnv>();
app.route("/auth", auth.router());
app.get("/me", auth.protect(), (c) => c.json(c.get("user")));
app.onError(auth.errorHandler());
export default app;createAuthHono generates an RSA-2048 key pair at startup and enables GET /keys automatically. Works with Node.js, Cloudflare Workers, Bun, and Deno — use client mode for edge runtimes that cannot reach a PostgreSQL database.
For detailed Hono docs see src/adapters/hono/README.md.
Elysia (server mode)
import { Elysia } from "elysia";
import { createAuthElysia } from "sentri/elysia";
import { PostgresDialect } from "kysely";
import pg from "pg";
const { Pool } = pg;
const auth = createAuthElysia({
mode: "server",
validRoles: ["user", "admin"] as const,
dialect: new PostgresDialect({
pool: new Pool({ connectionString: process.env.DATABASE_URL! }),
}),
});
await auth.migrate();
const app = new Elysia()
.onError(auth.errorHandler())
.group("/auth", (app) => app.use(auth.router()))
.use(auth.protect())
.get("/me", ({ user }) => user);
app.listen(3000);For detailed Elysia docs see src/adapters/elysia/README.md.
Koa (server mode)
import Koa from "koa";
import Router from "@koa/router";
import bodyParser from "koa-bodyparser";
import { createAuthKoa } from "sentri/koa";
const auth = createAuthKoa({
mode: "server",
validRoles: ["user", "admin"] as const,
db: { connectionString: process.env.DATABASE_URL! },
});
await auth.migrate();
const app = new Koa();
app.use(bodyParser());
app.use(auth.errorHandler());
const rootRouter = new Router();
rootRouter.use("/auth", auth.router().routes(), auth.router().allowedMethods());
rootRouter.get("/me", auth.protect(), (ctx) => {
ctx.body = ctx.state.user;
});
app.use(rootRouter.routes());
app.use(rootRouter.allowedMethods());
app.listen(3000);For detailed Koa docs see src/adapters/koa/README.md.
Client mode (any framework, any server)
import { createAuthExpress } from "sentri/express";
const auth = createAuthExpress({
mode: "client",
keyUri: "https://auth.myapp.com/auth/keys",
});Server Mode — Custom Dialect
import express from "express";
import { createAuth } from "sentri";
import { PostgresDialect } from "kysely"; // kysely is bundled, no install needed
import { Pool } from "pg"; // pg is bundled, no install needed
const auth = createAuth({
mode: "server",
dialect: new PostgresDialect({
pool: new Pool({ connectionString: process.env.DATABASE_URL }),
}),
secret: process.env.JWT_PRIVATE_KEY!, // RSA private key PEM (RS256) or plain string (HS256)
algorithm: "RS256",
validRoles: ["user", "admin"] as const,
});
const app = express();
app.use(express.json());
await auth.migrate();
app.use("/auth", auth.router());
app.use(auth.errorHandler());
app.listen(3000);Client Mode (Other Apps)
import express from "express";
import { createAuth } from "sentri";
const auth = createAuth({
mode: "client",
keyUri: "https://auth.myapp.com/auth/keys",
});
const app = express();
app.get("/products", auth.protect(), auth.authorize("admin"), handler);
app.get(
"/orders",
auth.protect(),
auth.permit((req) => req.user!.id === req.params.userId),
handler,
);
app.use(auth.errorHandler());Server Mode
Server mode manages users, sessions, and tokens entirely within Sentri. The user provides a database dialect; Sentri handles the schema.
createAuthServer(options) — PostgreSQL shortcut
import { createAuthServer } from "sentri/express";
const auth = createAuthServer({
validRoles: ["user", "admin"] as const,
// Connection string
db: { connectionString: process.env.DATABASE_URL!, max: 10 },
// — or individual params —
// db: { host: 'localhost', port: 5432, database: 'mydb', user: 'app', password: 'secret' },
// Optional
accessExpiresIn: "15m",
refreshExpiresIn: "7d",
saltRounds: 12,
apiKey: process.env.REGISTER_API_KEY,
redisUrl: process.env.REDIS_URL, // enables Redis-backed idempotency cache
rateLimit: { maxLoginAttempts: 5, maxRegisterAttempts: 5, durationSeconds: 900 }, // default 5 attempts per 15 min
});createAuth(config) — Full config
import { createAuth } from "sentri/express";
createAuth({
mode: "server",
dialect, // required — Kysely Dialect
secret: process.env.JWT_SECRET!, // required — RSA private key PEM (RS256) or string (HS256)
validRoles: ["user", "admin"] as const, // required
algorithm: "RS256", // default: 'HS256' | also: 'HS384','HS512','RS384','RS512'
accessExpiresIn: "15m", // default: '15m'
refreshExpiresIn: "7d", // default: '7d'
saltRounds: 12, // default: 12 (bcrypt rounds, 10–31)
apiKey: process.env.REGISTER_API_KEY, // restricts POST /register
redisUrl: process.env.REDIS_URL, // Redis URL for idempotency cache
// -- Rate Limiting (optional) -----------------------------------------------
// rateLimit: { maxLoginAttempts: 5, maxRegisterAttempts: 5, durationSeconds: 900 }, // optional rate limiting config
cookie: { secure: true }, // httpOnly refresh token cookie
accessCookie: { secure: true }, // non-httpOnly access token cookie (SPA)
hooks: { onLogin, onFailedLogin, onLogout },
isTokenRevoked: async (sessionId) =>
await redis.sismember("revoked", sessionId),
router: {
// override built-in service functions
login,
register,
refresh,
logout,
logoutAll,
assignRoles,
bulkCreateIdentifiers,
bulkUpdateIdentifiers,
bulkDeleteIdentifiers,
changePassword,
},
});Database Migration
// Call once at startup — uses IF NOT EXISTS, safe to repeat
await auth.migrate();Creates three tables:
sentri_users— id, password_hash, roles (JSON), created_atsentri_sessions— id, user_id, expires_at, created_atsentri_identifiers— id, user_id, type, value (globally unique), created_at
Client Mode
Client mode has no database. It fetches the auth server's public key and validates JWTs stateless.
createAuth({
mode: "client",
keyUri: "https://auth.myapp.com/auth/keys", // required
validRoles: ["admin", "user"], // optional — TypeScript type safety only
});Available methods: protect(), authorize(), permit(), errorHandler().
The public key is fetched once and cached for 1 hour. Token validation is fully stateless.
SSO Flow
[User] → POST /auth/login (Sentri Auth Server)
← { accessToken, user }
[User] → GET /products (App A — client mode)
Authorization: Bearer <accessToken>
← App A fetches public key from GET /auth/keys, verifies JWT
← 200 OK
For SSO, use algorithm: 'RS256' on the server (or createAuthServer() which defaults to RS256). This automatically adds GET /keys to the auth router, which returns the public key in JWKS format (RFC 7517).
GET /auth/keys → { keys: [{ kty, use, kid, n, e, ... }] }
Client apps point keyUri at this endpoint and receive the public key automatically.
Multi-Identifier
Each user can have multiple identifiers — email, username, phone number, or any custom type. All identifier values are globally unique regardless of type.
Login accepts any identifier value — Sentri searches all types automatically. No concept of "primary" exists; the JWT payload only contains { id, roles, sessionId }.
Registration
Provide at least one identifier.
POST /register
Content-Type: application/json
{
"identifiers": [
{ "type": "email", "value": "rizz@example.com" },
{ "type": "username", "value": "rizz" }
],
"password": "secret123",
"roles": ["user"]
}
Response:
{
"error": false,
"statusCode": 201,
"message": "User registered successfully",
"data": {
"user": {
"id": "uuid",
"roles": ["user"],
"identifiers": [
{ "id": "uuid-1", "type": "email", "value": "rizz@example.com" },
{ "id": "uuid-2", "type": "username", "value": "rizz" }
]
}
}
}Login
Send any of the user's identifier values — Sentri searches all types automatically.
POST /login
Content-Type: application/json
{ "identifier": "rizz", "password": "secret123" }
Bulk Create Identifiers
POST /me/identifiers
Authorization: Bearer <token>
Content-Type: application/json
{
"identifiers": [
{ "type": "phone", "value": "+628123456789" }
]
}
Bulk Update Identifiers
PUT /me/identifiers
Authorization: Bearer <token>
Content-Type: application/json
{
"identifiers": [
{ "id": "uuid-2", "type": "username", "value": "newrizz" }
]
}
Bulk Delete Identifiers
DELETE /me/identifiers
Authorization: Bearer <token>
Content-Type: application/json
{ "ids": ["uuid-2"] }
At least one identifier must remain after deletion.
Programmatic API
const auth = createAuth({ mode: 'server', ... });
// Register with multiple identifiers
await auth.register({
identifiers: [
{ type: 'email', value: 'rizz@example.com' },
{ type: 'username', value: 'rizz' },
],
password: 'secret123',
});
// Add identifiers after registration
await auth.bulkCreateIdentifiers(userId, [{ type: 'phone', value: '+628123456789' }]);
// Update identifiers
await auth.bulkUpdateIdentifiers(userId, [{ id: 'uuid-2', type: 'username', value: 'newrizz' }]);
// Delete identifiers
await auth.bulkDeleteIdentifiers(userId, ['uuid-2']);Configuration
algorithm
| Value | Type | Use case |
|---|---|---|
'HS256' |
Symmetric (default) | Single app, shared secret |
'RS256' |
Asymmetric | SSO — enables GET /keys |
When using RS256, secret must be a valid RSA private key in PEM format. createAuthServer() generates the key pair automatically.
Cookie Strategy (SPA)
createAuth({
mode: "server",
cookie: { secure: true }, // httpOnly refresh token
accessCookie: { secure: true }, // non-httpOnly access token (readable by JS)
});After login, both cookies are set automatically. protect() reads the access token from the cookie when no Authorization header is present.
Endpoints
auth.router() mounts these endpoints:
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/register |
— | Create a user (requires X-Api-Key when apiKey is set) |
POST |
/login |
— | Authenticate, receive tokens |
POST |
/refresh |
— | Rotate refresh token |
POST |
/logout |
— | Invalidate current session |
POST |
/logout-all |
✓ | Invalidate all sessions |
GET |
/me |
✓ | Return authenticated user with all identifiers |
POST |
/me/identifiers |
✓ self | Add identifiers in bulk |
PUT |
/me/identifiers |
✓ self | Update identifiers in bulk |
DELETE |
/me/identifiers |
✓ self | Delete identifiers in bulk |
PATCH |
/me/password |
✓ self | Change password — revokes all sessions |
POST |
/users/:userId/roles |
✓ admin | Assign roles to user |
GET |
/keys |
— | Public key in JWKS format (RS256 only) |
All responses use the envelope:
{ "error": false, "statusCode": 200, "message": "...", "data": { ... } }Change Password
PATCH /me/password
Authorization: Bearer <token>
Content-Type: application/json
{ "currentPassword": "old-pass", "newPassword": "new-pass" }
Changing the password revokes all existing sessions. The user must log in again after this call.
Middleware
auth.protect()
Verifies the JWT and sets the user on the request context. In server mode, also performs silent token refresh when the access token expires.
Token is read from Authorization: Bearer <token> header or access_token cookie.
// Express
app.get("/dashboard", auth.protect(), (req, res) => res.json(req.user));
// req.user: { id, roles }
// Fastify
app.get(
"/dashboard",
{ preHandler: auth.protect() },
async (request) => request.user,
);
// Hono
app.get("/dashboard", auth.protect(), (c) => c.json(c.get("user")));auth.authorize(...roles)
Role-based access — must follow protect().
// Express / Fastify preHandler / Hono — same API
app.delete("/posts/:id", auth.protect(), auth.authorize("admin"), handler);auth.permit(check | options)
Resource-level permission — must follow protect(). The check function receives the request context object of each framework.
// Express
app.put(
"/users/:id",
auth.protect(),
auth.permit((req) => req.user!.id === req.params.id),
handler,
);
// Hono
app.put(
"/users/:id",
auth.protect(),
auth.permit((c) => c.get("user")!.id === c.req.param("id")),
handler,
);
// Role bypass + ownership check
auth.permit({
roles: ["admin"],
check: async (req) => {
const post = await db.posts.findById(req.params.id);
return post?.authorId === req.user!.id;
},
});auth.rateLimiter(options)
Applies rate limiting to any custom route using the unified rate limiter cache (in-memory or Redis).
app.post(
"/send-email",
auth.rateLimiter({ maxAttempts: 3, durationSeconds: 60, keyPrefix: "email" }),
handler
);Token Utilities
Available on ServerAuthClient only:
const auth = createAuth({ mode: 'server', ... });
// Sign
const accessToken = auth.signAccessToken({ id, roles });
const refreshToken = auth.signRefreshToken(sessionId);
// Verify
const user = auth.verifyAccessToken(accessToken);
const { sessionId } = auth.verifyRefreshToken(refreshToken);
// Password
const hash = await auth.hashPassword('secret123');
const valid = await auth.verifyPassword('secret123', hash);
// Extract raw token from request
const token = auth.getCurrentAccessToken(req);Error Handling
// Express — must be last
app.use("/auth", auth.router());
app.use("/api", apiRouter);
app.use(auth.errorHandler());
// Fastify
app.setErrorHandler(auth.errorHandler());
// Hono — mount via onError
app.route("/auth", auth.router());
app.onError(auth.errorHandler());| Code | HTTP | Meaning |
|---|---|---|
INVALID_CREDENTIALS |
401 | Wrong identifier or password |
USER_NOT_FOUND |
404 | User does not exist |
USER_ALREADY_EXISTS |
409 | Duplicate identifier at registration |
IDENTIFIER_NOT_FOUND |
404 | Referenced identifier ID does not exist or belongs to another user |
IDENTIFIER_ALREADY_EXISTS |
409 | Identifier value already taken by another user |
TOKEN_EXPIRED |
401 | JWT exp is in the past |
TOKEN_INVALID |
401 | Bad signature or malformed JWT |
UNAUTHORIZED |
401 | No valid token or session not found |
FORBIDDEN |
403 | Authenticated but missing required role |
INVALID_ROLE |
400 | Role not in validRoles |
VALIDATION_ERROR |
400 | Missing or invalid input |
RATE_LIMIT_EXCEEDED |
429 | Too many requests to an endpoint |
CONFIGURATION_ERROR |
500 | Invalid createAuth config |
Extending SentriError
import { SentriError } from "sentri";
class NotFoundError extends SentriError {
constructor(resource: string) {
super("NOT_FOUND", `${resource} not found`, 404);
}
}
// Caught automatically by auth.errorHandler()
app.get("/items/:id", auth.protect(), async (req, res) => {
const item = await db.items.findById(req.params.id);
if (!item) throw new NotFoundError("Item");
res.json(item);
});Request Idempotency
Repeat requests with the same X-Idempotency-Key header receive the cached response immediately (2xx only). Useful for POST/PUT/PATCH endpoints where clients may retry on network failure.
Via createAuthServer() (recommended)
const auth = createAuthServer({
validRoles: ["user", "admin"] as const,
db: { connectionString: process.env.DATABASE_URL! },
redisUrl: process.env.REDIS_URL, // omit for in-memory cache
});
// Mount before your routes
app.use(auth.idempotencyMiddleware());
// Override TTL or methods
app.use(auth.idempotencyMiddleware({ ttl: 60_000 }));When redisUrl is set in server config, the middleware automatically uses Redis — no extra config needed.
Standalone (without createAuthServer)
import { createIdempotencyMiddleware } from "sentri";
// In-memory (single process)
app.use(createIdempotencyMiddleware({ ttl: 300_000 }));
// Redis (multi-process)
app.use(createIdempotencyMiddleware({ redisUrl: "redis://localhost:6379" }));Options
| Option | Default | Description |
|---|---|---|
ttl |
300_000 |
Cache TTL in milliseconds |
header |
'X-Idempotency-Key' |
Header name to read the key from |
methods |
['POST','PUT','PATCH'] |
HTTP methods to apply idempotency to |
maxSize |
10_000 |
Max in-memory entries (ignored when redisUrl is set) |
redisUrl |
— | Redis connection URL for multi-process cache |
Logger
Sentri produces structured JSON log entries for every auth event. Logging is opt-in — when no logger is configured, Sentri is completely silent (zero overhead).
Activating the logger
Pass any object that implements { info, warn, error } via the logger field in your config.
pino (recommended for production):
import pino from "pino";
const auth = createAuth({
mode: "server",
// ...other config
logger: pino(),
});winston:
import winston from "winston";
const auth = createAuth({
mode: "server",
// ...other config
logger: winston.createLogger({
transports: [new winston.transports.Console()],
}),
});console (zero setup, good for development):
const auth = createAuth({
mode: "server",
// ...other config
logger: console,
});Works identically in client mode:
const auth = createAuth({
mode: "client",
keyUri: "https://auth.myapp.com/auth/keys",
logger: pino(),
});Customising the service name
By default every log entry contains "service": "sentri". Override it with loggerService:
const auth = createAuth({
// ...
logger: pino(),
loggerService: "auth-service",
});Request ID correlation
If you mount a request-ID middleware before Sentri, the requestId is automatically included in every log entry for that request:
import { randomUUID } from "crypto";
app.use((req, _res, next) => {
req.requestId = randomUUID();
next();
});
app.use("/auth", auth.router());Sample pino output:
{"level":30,"time":1719484800000,"service":"sentri","event":"auth.login.success","userId":"usr_abc","duration_ms":42,"requestId":"d4e5f6"}
{"level":40,"time":1719484801000,"service":"sentri","event":"auth.authorize.denied","userId":"usr_abc","userRoles":["user"],"requiredRoles":["admin"],"requestId":"d4e5f7"}Log events
| Event | Level | Emitted by | Key fields |
|---|---|---|---|
auth.protect.success |
info | protect() |
userId, mode |
auth.protect.failure |
warn | protect() |
errorCode, mode |
auth.protect.token_revoked |
warn | protect() |
userId, mode |
auth.protect.auto_refresh |
info | protect() |
userId, mode |
auth.authorize.passed |
info | authorize() |
userId, userRoles, requiredRoles |
auth.authorize.denied |
warn | authorize() |
userId, userRoles, requiredRoles |
auth.authorize.unauthenticated |
warn | authorize() |
requiredRoles |
auth.permit.passed |
info | permit() |
userId |
auth.permit.denied |
warn | permit() |
userId |
auth.permit.role_bypass |
info | permit() |
userId, bypassedByRole |
auth.permit.unauthenticated |
warn | permit() |
— |
auth.register.success |
info | router | userId, duration_ms |
auth.register.failure |
warn | router | errorCode, duration_ms |
auth.login.success |
info | router | userId, duration_ms |
auth.login.failure |
warn | router | errorCode, duration_ms |
auth.refresh.success |
info | router | userId, duration_ms |
auth.refresh.failure |
warn | router | errorCode, duration_ms |
auth.logout |
info | router | duration_ms |
auth.logout_all |
info | router | userId, duration_ms |
auth.password.changed |
info | router | userId, duration_ms |
auth.password.change_failure |
warn | router | userId, errorCode, duration_ms |
auth.roles.assigned |
info | router | targetUserId, roles, duration_ms |
auth.roles.assign_failure |
warn | router | targetUserId, errorCode, duration_ms |
auth.identifiers.created |
info | router | userId, count, duration_ms |
auth.identifiers.updated |
info | router | userId, count, duration_ms |
auth.identifiers.deleted |
info | router | userId, duration_ms |
All entries include service (configurable via loggerService) and requestId when available.
SentriLogger interface
import type { SentriLogger } from "sentri";
const myLogger: SentriLogger = {
info(data: Record<string, unknown>) {
/* ... */
},
warn(data: Record<string, unknown>) {
/* ... */
},
error(data: Record<string, unknown>) {
/* ... */
},
};Migration Guide
5.0.0 Breaking Changes
| What changed | Action required |
|---|---|
is_primary column removed from sentri_identifiers |
Drop and recreate tables — run auth.migrate() after |
AuthUser no longer has identifier / identifierType |
Update any code reading req.user — shape is now { id, roles } |
JWT payload no longer includes identifier / identifierType |
Any code decoding the token directly must drop these fields |
PATCH /me/identifiers/primary endpoint removed |
No replacement — the concept of primary is gone |
changePrimaryIdentifier() removed from ServerAuthClient |
Delete call sites |
ChangePrimaryResult type removed |
Delete references from type imports |
bulkDeleteIdentifiers guard changed |
Old: "cannot delete primary". New: "must keep at least one identifier" |
4.0.0 Breaking Changes
| What changed | Action required |
|---|---|
sentri_users no longer has an identifier column |
Drop and recreate tables — identities now live in sentri_identifiers |
New sentri_identifiers table |
Created automatically by auth.migrate() |
RegisterInput.identifier → RegisterInput.identifiers (array) |
Update register calls to pass identifiers: [{ type, value }] |
PATCH /me/identifier removed |
Use PUT /me/identifiers for updates |
changeIdentifier() removed from ServerAuthClient |
Use bulkUpdateIdentifiers() |
New error codes: IDENTIFIER_NOT_FOUND, IDENTIFIER_ALREADY_EXISTS |
Handle these in your error handlers as needed |
3.0.0 Breaking Changes
| What changed | Action required |
|---|---|
createAuth now requires mode: 'server' or mode: 'client' |
Add mode: 'server' to existing configs |
adapter field removed — Sentri owns the schema |
Remove adapter, add dialect (Kysely Dialect) |
algorithm now supports 'RS256' | 'RS384' | 'RS512' |
Optional — HS256 still default |
AuthError renamed to SentriError |
Update imports: import { SentriError } from 'sentri' |
AUTH_ERROR_STATUS renamed to SENTRI_ERROR_STATUS |
Update references |
npx sentri generate removed |
Run await auth.migrate() at startup instead |
| Templates directory removed | No longer needed |