sentri 2.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # sentri
2
2
 
3
- Auth and authorization library for Express + PostgreSQL. Provides stateless JWT authentication, automatic token refresh, cookie-based token storage, request idempotency, role-based access control, fine-grained permission checks, and a pre-built Express router — all behind a single typed client.
3
+ Auth and authorization library for Express. Supports two modes:
4
+
5
+ - **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.
6
+ - **Client mode** — used by other apps to validate tokens issued by the auth server, without a database.
4
7
 
5
8
  ---
6
9
 
@@ -8,22 +11,16 @@ Auth and authorization library for Express + PostgreSQL. Provides stateless JWT
8
11
 
9
12
  - [Installation](#installation)
10
13
  - [Quick Start](#quick-start)
11
- - [CLI](#cli)
14
+ - [Server Mode](#server-mode)
15
+ - [Client Mode](#client-mode)
16
+ - [SSO Flow](#sso-flow)
17
+ - [Multi-Identifier](#multi-identifier)
12
18
  - [Configuration](#configuration)
13
- - [Token Storage](#token-storage)
14
- - [Stateless Access Tokens](#stateless-access-tokens)
15
- - [Automatic Token Refresh](#automatic-token-refresh)
16
- - [Request Idempotency](#request-idempotency)
17
- - [Lifecycle Hooks](#lifecycle-hooks)
18
- - [Immediate Token Revocation](#immediate-token-revocation-istokenrevoked)
19
- - [apiKey — Restricting Registration](#apikey--restricting-registration)
20
- - [Custom Route Handlers](#custom-route-handlers)
21
- - [Adapter Interface](#adapter-interface)
22
- - [Pre-built Router](#pre-built-router)
19
+ - [Endpoints](#endpoints)
23
20
  - [Middleware](#middleware)
24
- - [Programmatic API](#programmatic-api)
25
- - [Types](#types)
21
+ - [Token Utilities](#token-utilities)
26
22
  - [Error Handling](#error-handling)
23
+ - [Request Idempotency](#request-idempotency)
27
24
  - [Migration Guide](#migration-guide)
28
25
 
29
26
  ---
@@ -34,522 +31,399 @@ Auth and authorization library for Express + PostgreSQL. Provides stateless JWT
34
31
  npm install sentri
35
32
  ```
36
33
 
37
- **Peer dependency:** `express >= 4.0.0`
38
-
39
- ---
40
-
41
- ## Quick Start
34
+ `kysely` and `pg` are bundled with sentri — no separate installation needed for PostgreSQL.
42
35
 
43
- ### 1. Generate templates
36
+ For other databases, install the driver:
44
37
 
45
38
  ```bash
46
- # Prisma
47
- npx sentri generate prisma
39
+ # MySQL
40
+ npm install mysql2
48
41
 
49
- # Drizzle
50
- npx sentri generate drizzle
42
+ # SQLite
43
+ npm install better-sqlite3
51
44
  ```
52
45
 
53
- This creates:
46
+ **Peer dependency:** `express >= 4.0.0`
54
47
 
55
- ```
56
- src/lib/
57
- index.ts ← barrel export
58
- sentri/
59
- adapter.ts ← AuthAdapter implementation
60
- auth.ts ← configured auth client
61
- schema.ts ← table definitions (Drizzle only)
62
- prisma/
63
- schema.prisma ← Prisma models (Prisma only, created or appended)
64
- ```
48
+ ---
49
+
50
+ ## Quick Start
65
51
 
66
- ### 2. Mount the router and error handler
52
+ ### Server Mode PostgreSQL (recommended)
67
53
 
68
54
  ```typescript
69
55
  import express from 'express';
70
- import { auth } from './lib/sentri/auth.js';
71
- import { createIdempotencyMiddleware } from 'sentri';
56
+ import { createAuthServer } from 'sentri';
57
+
58
+ const auth = createAuthServer({
59
+ validRoles: ['user', 'admin'] as const,
60
+ db: { connectionString: process.env.DATABASE_URL! },
61
+ });
72
62
 
73
63
  const app = express();
74
64
  app.use(express.json());
65
+ app.use(auth.idempotencyMiddleware());
75
66
 
76
- // Optional: make POST/PUT/PATCH operations idempotent
77
- app.use(createIdempotencyMiddleware());
78
-
67
+ await auth.migrate();
79
68
  app.use('/auth', auth.router());
80
69
 
81
- // ... your routes ...
82
-
83
- // Must be last — catches SentriError from sentri and your own subclasses
70
+ app.get('/me', auth.protect(), (req, res) => res.json(req.user));
84
71
  app.use(auth.errorHandler());
85
72
  app.listen(3000);
86
73
  ```
87
74
 
88
- Done. All endpoints are available at `/auth/*`.
89
-
90
- ---
91
-
92
- ## CLI
75
+ `createAuthServer()` generates an RSA-2048 key pair at startup and enables `GET /keys` automatically.
93
76
 
94
- ### `sentri generate <prisma|drizzle>`
77
+ ### Server Mode — Custom Dialect
95
78
 
96
- Generates adapter, auth config, and schema templates in one command.
79
+ ```typescript
80
+ import express from 'express';
81
+ import { createAuth } from 'sentri';
82
+ import { PostgresDialect } from 'kysely'; // kysely is bundled, no install needed
83
+ import { Pool } from 'pg'; // pg is bundled, no install needed
84
+
85
+ const auth = createAuth({
86
+ mode: 'server',
87
+ dialect: new PostgresDialect({ pool: new Pool({ connectionString: process.env.DATABASE_URL }) }),
88
+ secret: process.env.JWT_PRIVATE_KEY!, // RSA private key PEM (RS256) or plain string (HS256)
89
+ algorithm: 'RS256',
90
+ validRoles: ['user', 'admin'] as const,
91
+ });
97
92
 
98
- ```bash
99
- npx sentri generate prisma
100
- npx sentri generate drizzle
93
+ const app = express();
94
+ app.use(express.json());
95
+ await auth.migrate();
96
+ app.use('/auth', auth.router());
97
+ app.use(auth.errorHandler());
98
+ app.listen(3000);
101
99
  ```
102
100
 
103
- **What gets created:**
104
-
105
- | File | Behavior |
106
- |---|---|
107
- | `src/lib/sentri/adapter.ts` | Created fresh (error if exists) |
108
- | `src/lib/sentri/auth.ts` | Created fresh (error if exists) |
109
- | `src/lib/sentri/schema.ts` | Drizzle only — created fresh or tables appended |
110
- | `prisma/schema.prisma` | Prisma only — created fresh or models appended |
111
- | `src/lib/index.ts` | Created fresh, skipped if already exists |
112
-
113
- ---
114
-
115
- ## Configuration
101
+ ### Client Mode (Other Apps)
116
102
 
117
103
  ```typescript
104
+ import express from 'express';
118
105
  import { createAuth } from 'sentri';
119
106
 
120
- export const auth = createAuth({
121
- secret: process.env.JWT_SECRET!, // required — keep in env, min 32 chars
122
- validRoles: ['user', 'admin'] as const, // required — use `as const` for type safety
123
- adapter: myAdapter, // required — see Adapter Interface
124
-
125
- // optional
126
- accessExpiresIn: '5m', // default: '15m' — short is safe: auto-refresh handles it
127
- refreshExpiresIn: '7d', // default: '7d'
128
- algorithm: 'HS256', // default: 'HS256' — also 'HS384' | 'HS512'
129
- saltRounds: 12, // default: 12 (bcrypt rounds, min 10)
130
-
131
- // API key for POST /register — see apiKey section
132
- apiKey: process.env.REGISTER_API_KEY, // optional
133
-
134
- // Access token cookie — non-httpOnly so browser JS can read it.
135
- // When set, protect() reads from this cookie when no Authorization header is present.
136
- accessCookie: {
137
- secure: process.env.NODE_ENV === 'production',
138
- // name: 'access_token', // default: 'access_token'
139
- // sameSite: 'strict', // default: 'strict'
140
- // path: '/', // default: '/'
141
- },
107
+ const auth = createAuth({
108
+ mode: 'client',
109
+ keyUri: 'https://auth.myapp.com/auth/keys',
110
+ });
142
111
 
143
- // Refresh token cookie — httpOnly (not readable by JS).
144
- cookie: {
145
- secure: process.env.NODE_ENV === 'production',
146
- // name: 'refresh_token', // default: 'refresh_token'
147
- // httpOnly: true, // default: true
148
- // sameSite: 'strict', // default: 'strict'
149
- // path: '/', // default: '/'
150
- },
112
+ const app = express();
113
+ app.get('/products', auth.protect(), auth.authorize('admin'), handler);
114
+ app.get('/orders', auth.protect(), auth.permit(req => req.user!.id === req.params.userId), handler);
151
115
 
152
- // Lifecycle hooks — fire-and-forget, rejections are swallowed.
153
- // hooks: {
154
- // onLogin: async (user) => auditLog.record('login', user.id),
155
- // onFailedLogin: (identifier) => rateLimiter.hit(`login:${identifier}`),
156
- // onLogout: async (userId) => cache.invalidate(userId),
157
- // },
158
-
159
- // Optional immediate revocation — called on every protect() invocation.
160
- // isTokenRevoked: async (sessionId) =>
161
- // await redis.sismember('revoked_sessions', sessionId),
162
-
163
- // router: { // optional — replace built-in service logic per route
164
- // login: async (input) => { ... },
165
- // register: async (input) => { ... },
166
- // refresh: async (refreshToken) => { ... },
167
- // logout: async (refreshToken) => { ... },
168
- // logoutAll: async (userId) => { ... },
169
- // assignRoles: async (userId, roles) => { ... },
170
- // },
171
- });
116
+ app.use(auth.errorHandler());
172
117
  ```
173
118
 
174
- `accessExpiresIn` and `refreshExpiresIn` accept duration strings (`'5m'`, `'1h'`, `'7d'`, `'30d'`) or seconds as a number.
175
-
176
119
  ---
177
120
 
178
- ## Token Storage
121
+ ## Server Mode
179
122
 
180
- sentri uses a two-cookie strategy for SPAs:
123
+ Server mode manages users, sessions, and tokens entirely within Sentri. The user provides a database dialect; Sentri handles the schema.
181
124
 
182
- | Cookie | `httpOnly` | Readable by JS | Purpose |
183
- |---|---|---|---|
184
- | `access_token` | **false** | ✅ Yes | Carries the JWT for authenticated requests |
185
- | `refresh_token` | **true** | ❌ No | Used to silently rotate the access token |
186
-
187
- ### Setting up cookie storage
188
-
189
- Enable both cookies in your config:
125
+ ### `createAuthServer(options)` PostgreSQL shortcut
190
126
 
191
127
  ```typescript
192
- export const auth = createAuth({
193
- accessExpiresIn: '5m',
194
- accessCookie: { secure: process.env.NODE_ENV === 'production' },
195
- cookie: { secure: process.env.NODE_ENV === 'production' },
196
- // ...
197
- });
198
- ```
128
+ import { createAuthServer } from 'sentri';
129
+
130
+ const auth = createAuthServer({
131
+ validRoles: ['user', 'admin'] as const,
199
132
 
200
- After `/login`, the router sets both cookies automatically. `protect()` reads the access token from the cookie when no `Authorization: Bearer` header is present, so your SPA needs zero manual header management.
133
+ // Connection string
134
+ db: { connectionString: process.env.DATABASE_URL!, max: 10 },
201
135
 
202
- ### Reading the access token in the browser
136
+ // or individual params
137
+ // db: { host: 'localhost', port: 5432, database: 'mydb', user: 'app', password: 'secret' },
203
138
 
204
- ```javascript
205
- // Read from document.cookie
206
- const token = document.cookie
207
- .split('; ')
208
- .find(c => c.startsWith('access_token='))
209
- ?.split('=')[1];
139
+ // Optional
140
+ accessExpiresIn: '15m',
141
+ refreshExpiresIn: '7d',
142
+ saltRounds: 12,
143
+ apiKey: process.env.REGISTER_API_KEY,
144
+ redisUrl: process.env.REDIS_URL, // enables Redis-backed idempotency cache
145
+ });
210
146
  ```
211
147
 
212
- ### Reading the token server-side
148
+ ### `createAuth(config)` Full config
213
149
 
214
150
  ```typescript
215
- import { getCurrentAccessToken } from 'sentri';
216
-
217
- app.get('/debug', (req, res) => {
218
- // Checks Authorization header first, then access_token cookie
219
- const token = getCurrentAccessToken(req, config);
220
- res.json({ hasToken: !!token });
151
+ createAuth({
152
+ mode: 'server',
153
+ dialect, // required Kysely Dialect
154
+ secret: process.env.JWT_SECRET!, // required RSA private key PEM (RS256) or string (HS256)
155
+ validRoles: ['user', 'admin'] as const, // required
156
+
157
+ algorithm: 'RS256', // default: 'HS256' | also: 'HS384','HS512','RS384','RS512'
158
+ accessExpiresIn: '15m', // default: '15m'
159
+ refreshExpiresIn: '7d', // default: '7d'
160
+ saltRounds: 12, // default: 12 (bcrypt rounds, 10–31)
161
+ apiKey: process.env.REGISTER_API_KEY, // restricts POST /register
162
+ redisUrl: process.env.REDIS_URL, // Redis URL for idempotency cache
163
+ cookie: { secure: true }, // httpOnly refresh token cookie
164
+ accessCookie: { secure: true }, // non-httpOnly access token cookie (SPA)
165
+ hooks: { onLogin, onFailedLogin, onLogout },
166
+ isTokenRevoked: async (sessionId) => await redis.sismember('revoked', sessionId),
167
+ router: { // override built-in service functions
168
+ login, register, refresh, logout, logoutAll, assignRoles,
169
+ bulkCreateIdentifiers, bulkUpdateIdentifiers, bulkDeleteIdentifiers,
170
+ changePrimaryIdentifier, changePassword,
171
+ },
221
172
  });
222
-
223
- // Or via the auth client (pre-bound to config):
224
- const token = auth.getCurrentAccessToken(req);
225
173
  ```
226
174
 
227
- ---
228
-
229
- ## Stateless Access Tokens
175
+ ### Database Migration
230
176
 
231
- Access tokens are validated **stateless** — by JWT signature and `exp` claim only. No database lookup occurs on every protected request, which keeps `protect()` fast regardless of load.
232
-
233
- ```
234
- Request → protect() verifies JWT signature + expiry → ✓ allowed
235
- (no DB round-trip per request)
177
+ ```typescript
178
+ // Call once at startup — uses IF NOT EXISTS, safe to repeat
179
+ await auth.migrate();
236
180
  ```
237
181
 
238
- **Trade-off:** After logout, an access token remains technically valid until it expires (`accessExpiresIn`). Keep this window short (`'5m'`) to limit exposure. Refresh tokens are still session-bound, so a revoked refresh token cannot obtain new access tokens.
239
-
240
- ```
241
- Logout → session deleted from DB
242
- Request with old access token → accepted until exp (≤ 5m)
243
- Request with old refresh token → rejected immediately (session gone)
244
- ```
182
+ Creates three tables:
183
+ - `sentri_users` — id, password_hash, roles (JSON), created_at
184
+ - `sentri_sessions` — id, user_id, expires_at, created_at
185
+ - `sentri_identifiers` id, user_id, type, value (globally unique), is_primary, created_at
245
186
 
246
187
  ---
247
188
 
248
- ## Automatic Token Refresh
249
-
250
- When `protect()` encounters an expired access token, it performs one silent refresh before returning an error:
189
+ ## Client Mode
251
190
 
252
- 1. Reads the refresh token from the httpOnly cookie.
253
- 2. Rotates the session (deletes old, creates new).
254
- 3. Issues new access + refresh tokens.
255
- 4. Updates both cookies on the response.
256
- 5. Writes the new access token to the `X-New-Access-Token` response header.
257
- 6. Calls `next()` — the handler runs normally without any knowledge of the refresh.
191
+ Client mode has no database. It fetches the auth server's public key and validates JWTs stateless.
258
192
 
259
- If the refresh also fails (session expired or revoked), the middleware calls `next(SentriError)` with `UNAUTHORIZED` and the user must log in again.
260
-
261
- ```
262
- Request with expired access token
263
- protect() tries refresh cookie
264
- → refresh succeeds → new tokens set → request continues transparently
265
- → refresh fails → 401 UNAUTHORIZED — user must login
193
+ ```typescript
194
+ createAuth({
195
+ mode: 'client',
196
+ keyUri: 'https://auth.myapp.com/auth/keys', // required
197
+ validRoles: ['admin', 'user'], // optional — TypeScript type safety only
198
+ });
266
199
  ```
267
200
 
268
- **Client-side:** Check the `X-New-Access-Token` header on every response to detect a silent refresh and update any in-memory token copy:
201
+ Available methods: `protect()`, `authorize()`, `permit()`, `errorHandler()`.
269
202
 
270
- ```javascript
271
- const newToken = response.headers.get('X-New-Access-Token');
272
- if (newToken) currentAccessToken = newToken;
273
- ```
274
-
275
- When `accessCookie` is configured the cookie is updated automatically, so in-memory management is optional.
203
+ The public key is fetched once and cached for 1 hour. Token validation is fully stateless.
276
204
 
277
205
  ---
278
206
 
279
- ## Request Idempotency
280
-
281
- `createIdempotencyMiddleware()` makes `POST`, `PUT`, and `PATCH` operations idempotent. When a client sends the same `X-Idempotency-Key` header twice, the second request receives the cached response without re-executing the handler.
282
-
283
- ```typescript
284
- import { createIdempotencyMiddleware } from 'sentri';
207
+ ## SSO Flow
285
208
 
286
- // Apply globally (before routes)
287
- app.use(createIdempotencyMiddleware());
288
-
289
- // Or scoped with custom options
290
- apiRouter.use(createIdempotencyMiddleware({
291
- ttl: 60_000, // cache TTL in ms — default: 300_000 (5 min)
292
- header: 'X-Request-Id', // key header name — default: 'X-Idempotency-Key'
293
- methods: ['POST'], // methods to watch — default: ['POST','PUT','PATCH']
294
- }));
295
209
  ```
210
+ [User] → POST /auth/login (Sentri Auth Server)
211
+ ← { accessToken, user }
296
212
 
297
- ### How it works
298
-
299
- | Request | Key present | Cache hit | Behaviour |
300
- |---|---|---|---|
301
- | POST | No | — | Passes through unchanged |
302
- | POST | Yes | No | Handler runs; response is cached (2xx only) |
303
- | POST | Yes | Yes | Cached response returned immediately; `X-Idempotent-Replayed: true` header added |
213
+ [User] → GET /products (App A — client mode)
214
+ Authorization: Bearer <accessToken>
215
+ ← App A fetches public key from GET /auth/keys, verifies JWT
216
+ ← 200 OK
217
+ ```
304
218
 
305
- The idempotency key is also attached as `req.requestId` and echoed in the `X-Request-Id` response header for all requests that include it.
219
+ 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).
306
220
 
307
- ```typescript
308
- app.post('/orders', createIdempotencyMiddleware(), async (req, res) => {
309
- console.log(req.requestId); // the X-Idempotency-Key value
310
- const order = await db.orders.create(req.body);
311
- res.status(201).json({ error: false, data: order });
312
- // On retry with same key → instant 201 from cache, no DB write
313
- });
221
+ ```
222
+ GET /auth/keys → { keys: [{ kty, use, kid, n, e, ... }] }
314
223
  ```
315
224
 
316
- **Note:** The cache is in-memory and per-process. For multi-process deployments (clusters, containers) use a shared cache layer such as Redis.
225
+ Client apps point `keyUri` at this endpoint and receive the public key automatically.
317
226
 
318
227
  ---
319
228
 
320
- ## Lifecycle Hooks
229
+ ## Multi-Identifier
321
230
 
322
- `hooks` lets you fire side effects at key points in the authentication flow. Every hook is optional and runs fire-and-forget a rejected hook Promise is silently swallowed so a broken hook can never abort or delay a login request.
231
+ Each user can have multiple identifiersemail, username, phone number, or any custom type. All identifier values are **globally unique** regardless of type.
323
232
 
324
- ```typescript
325
- export const auth = createAuth({
326
- // ...
327
- hooks: {
328
- /** Called after every successful login. */
329
- onLogin: async (user) => {
330
- await auditLog.record('login', { userId: user.id, identifier: user.identifier });
331
- },
233
+ One identifier per user is marked as **primary** and its value is embedded in the JWT payload. Regardless of which identifier a user logs in with, the JWT always contains the primary identifier.
332
234
 
333
- /** Called after every failed login attempt. */
334
- onFailedLogin: (identifier, error) => {
335
- rateLimiter.hit(`login:${identifier}`);
336
- logger.warn('Login failed', { identifier, code: error.code });
337
- },
235
+ ### Registration
338
236
 
339
- /** Called after logout (single session) and logout-all. */
340
- onLogout: async (userId) => {
341
- await cache.invalidate(userId);
342
- },
343
- },
344
- });
345
- ```
237
+ Provide at least one identifier. The first entry becomes the primary.
346
238
 
347
- ### `AuthHooks` interface
239
+ ```
240
+ POST /register
241
+ Content-Type: application/json
348
242
 
349
- | Hook | When | Receives |
350
- |---|---|---|
351
- | `onLogin` | After successful login | `AuthUser` |
352
- | `onFailedLogin` | After failed login (wrong password or unknown identifier) | `identifier: string`, `error: SentriError` |
353
- | `onLogout` | After `POST /logout-all` | `userId: string` |
243
+ {
244
+ "identifiers": [
245
+ { "type": "email", "value": "rizz@example.com" },
246
+ { "type": "username", "value": "rizz" }
247
+ ],
248
+ "password": "secret123",
249
+ "roles": ["user"]
250
+ }
251
+ ```
354
252
 
355
- > `POST /logout` (single session) does not fire `onLogout` — there is no authenticated user at that point. Use `onLogout` for "logout from all devices" events.
253
+ **Response:**
254
+ ```json
255
+ {
256
+ "error": false,
257
+ "statusCode": 201,
258
+ "message": "User registered successfully",
259
+ "data": {
260
+ "user": {
261
+ "id": "uuid",
262
+ "identifier": "rizz@example.com",
263
+ "identifierType": "email",
264
+ "roles": ["user"],
265
+ "identifiers": [
266
+ { "id": "uuid-1", "type": "email", "value": "rizz@example.com", "isPrimary": true },
267
+ { "id": "uuid-2", "type": "username", "value": "rizz", "isPrimary": false }
268
+ ]
269
+ }
270
+ }
271
+ }
272
+ ```
356
273
 
357
- ---
274
+ ### Login
358
275
 
359
- ## Immediate Token Revocation (`isTokenRevoked`)
276
+ Send any of the user's identifier values — Sentri searches all types automatically.
360
277
 
361
- By default, sentri uses stateless access tokens — no DB lookup per request. If you need to revoke a token immediately (e.g. after a security incident), supply `isTokenRevoked`:
278
+ ```
279
+ POST /login
280
+ Content-Type: application/json
362
281
 
363
- ```typescript
364
- export const auth = createAuth({
365
- // ...
366
- isTokenRevoked: async (sessionId) =>
367
- await redis.sismember('revoked_sessions', sessionId),
368
- });
282
+ { "identifier": "rizz", "password": "secret123" }
369
283
  ```
370
284
 
371
- After logout, add the `sessionId` to the revocation set:
285
+ ### Bulk Create Identifiers
372
286
 
373
- ```typescript
374
- // When handling a security incident — add to revoked set with a TTL
375
- // equal to the access token lifetime so memory stays bounded.
376
- await redis.sadd('revoked_sessions', sessionId);
377
- await redis.expire('revoked_sessions', 300); // 5 min — matches accessExpiresIn
378
287
  ```
288
+ POST /me/identifiers
289
+ Authorization: Bearer <token>
290
+ Content-Type: application/json
379
291
 
380
- `isTokenRevoked` is called on **every** `protect()` invocation. Keep it fast (a single Redis `SISMEMBER`) or the latency benefit of stateless JWTs is lost. If a short revocation window equal to `accessExpiresIn` is acceptable, omit this hook entirely.
381
-
382
- ---
292
+ {
293
+ "identifiers": [
294
+ { "type": "phone", "value": "+628123456789" }
295
+ ]
296
+ }
297
+ ```
383
298
 
384
- ## apiKey Restricting Registration
299
+ ### Bulk Update Identifiers
385
300
 
386
- By default `POST /register` is open to the public. Set `apiKey` in your config to lock the endpoint:
301
+ ```
302
+ PUT /me/identifiers
303
+ Authorization: Bearer <token>
304
+ Content-Type: application/json
387
305
 
388
- ```typescript
389
- export const auth = createAuth({
390
- // ...
391
- apiKey: process.env.REGISTER_API_KEY!,
392
- });
306
+ {
307
+ "identifiers": [
308
+ { "id": "uuid-2", "type": "username", "value": "newrizz" }
309
+ ]
310
+ }
393
311
  ```
394
312
 
395
- Requests to `POST /register` must then include the header:
313
+ ### Bulk Delete Identifiers
396
314
 
397
315
  ```
398
- X-Api-Key: <value of REGISTER_API_KEY>
316
+ DELETE /me/identifiers
317
+ Authorization: Bearer <token>
318
+ Content-Type: application/json
319
+
320
+ { "ids": ["uuid-2"] }
399
321
  ```
400
322
 
401
- Requests without the header, or with the wrong value, receive HTTP 401 `UNAUTHORIZED`. Keep the API key in an environment variable and share it only with trusted services.
323
+ At least one identifier must remain after deletion.
402
324
 
403
- ---
325
+ ### Change Primary Identifier
404
326
 
405
- ## Custom Route Handlers
327
+ ```
328
+ PATCH /me/identifiers/primary
329
+ Authorization: Bearer <token>
330
+ Content-Type: application/json
406
331
 
407
- The `router` field in config lets you replace the built-in service logic for individual routes while the router still handles request parsing, input validation, and response formatting.
332
+ { "id": "uuid-2" }
333
+ ```
408
334
 
409
- ```typescript
410
- import { createAuth, SentriError } from 'sentri';
411
- import type { AuthResult } from 'sentri';
335
+ The new primary value will be embedded in the JWT on the next login or token refresh.
412
336
 
413
- export const auth = createAuth({
414
- secret: process.env.JWT_SECRET!,
415
- validRoles: ['user', 'admin'] as const,
416
- adapter: myAdapter,
417
-
418
- router: {
419
- // Add an OTP check before issuing tokens
420
- login: async (input): Promise<AuthResult> => {
421
- const otpVerified = await redis.get(`otp:${input.identifier}`);
422
- if (!otpVerified) {
423
- return { success: false, error: new SentriError('INVALID_CREDENTIALS', 'OTP required') };
424
- }
425
- return defaultLogin(input);
426
- },
337
+ ### Programmatic API
427
338
 
428
- // Send a welcome email after registration
429
- register: async (input) => {
430
- const result = await defaultRegister(input);
431
- if (result.success) {
432
- await emailService.sendWelcome(input.identifier);
433
- }
434
- return result;
435
- },
436
- },
339
+ ```typescript
340
+ const auth = createAuth({ mode: 'server', ... });
341
+
342
+ // Register with multiple identifiers
343
+ await auth.register({
344
+ identifiers: [
345
+ { type: 'email', value: 'rizz@example.com' },
346
+ { type: 'username', value: 'rizz' },
347
+ ],
348
+ password: 'secret123',
437
349
  });
438
- ```
439
-
440
- ### Available handler signatures
441
350
 
442
- | Key | Signature | Must return |
443
- |---|---|---|
444
- | `register` | `(input: RegisterInput) => Promise<RegisterResult>` | `RegisterResult` |
445
- | `login` | `(input: LoginInput) => Promise<AuthResult>` | `AuthResult` |
446
- | `refresh` | `(refreshToken: string) => Promise<RefreshResult>` | `RefreshResult` |
447
- | `logout` | `(refreshToken: string \| undefined) => Promise<void>` | `void` |
448
- | `logoutAll` | `(userId: string) => Promise<void>` | `void` |
449
- | `assignRoles` | `(userId: string, roles: string[]) => Promise<AssignRolesResult>` | `AssignRolesResult` |
351
+ // Add identifiers after registration
352
+ await auth.bulkCreateIdentifiers(userId, [{ type: 'phone', value: '+628123456789' }]);
450
353
 
451
- ---
354
+ // Update identifiers
355
+ await auth.bulkUpdateIdentifiers(userId, [{ id: 'uuid-2', type: 'username', value: 'newrizz' }]);
452
356
 
453
- ## Adapter Interface
357
+ // Delete identifiers
358
+ await auth.bulkDeleteIdentifiers(userId, ['uuid-2']);
454
359
 
455
- The adapter connects sentri to your database. Implement `AuthAdapter` for any ORM or data layer.
456
-
457
- ```typescript
458
- import type { AuthAdapter } from 'sentri';
459
-
460
- const adapter: AuthAdapter = {
461
- user: {
462
- findByIdentifier(identifier: string): Promise<UserRecord | null>,
463
- findById(id: string): Promise<UserRecord | null>,
464
- create(data: CreateUserData): Promise<{ id: string }>,
465
- updateRoles(userId: string, roles: string[]): Promise<void>,
466
- },
467
- session: {
468
- create(data: { userId: string; expiresAt: Date }): Promise<{ id: string }>,
469
- findById(sessionId: string): Promise<(SessionRecord & { user: UserRecord }) | null>,
470
- delete(sessionId: string): Promise<void>,
471
- deleteAllForUser(userId: string): Promise<void>,
472
- },
473
- };
360
+ // Change primary
361
+ await auth.changePrimaryIdentifier(userId, 'uuid-3');
474
362
  ```
475
363
 
476
- `identifier` is a single string — the adapter decides what it maps to (email column, username column, phone, or a combined lookup). sentri never assumes the column name.
477
-
478
364
  ---
479
365
 
480
- ## Pre-built Router
366
+ ## Configuration
481
367
 
482
- `auth.router()` returns an Express Router with all standard endpoints. Every response uses this envelope:
368
+ ### `algorithm`
483
369
 
484
- ```typescript
485
- { error: boolean, statusCode: number, message: string, data: T | null }
486
- ```
370
+ | Value | Type | Use case |
371
+ |---|---|---|
372
+ | `'HS256'` | Symmetric (default) | Single app, shared secret |
373
+ | `'RS256'` | Asymmetric | SSO — enables `GET /keys` |
487
374
 
488
- ### Endpoints
375
+ When using RS256, `secret` must be a valid RSA private key in PEM format. `createAuthServer()` generates the key pair automatically.
489
376
 
490
- #### `POST /register`
377
+ ### Cookie Strategy (SPA)
491
378
 
492
- ```
493
- Headers: X-Api-Key: <key> (required when config.apiKey is set)
494
- Body: { identifier, password, roles?: string[] }
495
- Returns: { user: { id, identifier, roles } }
496
- Status: 201
379
+ ```typescript
380
+ createAuth({
381
+ mode: 'server',
382
+ cookie: { secure: true }, // httpOnly refresh token
383
+ accessCookie: { secure: true }, // non-httpOnly access token (readable by JS)
384
+ });
497
385
  ```
498
386
 
499
- #### `POST /login`
387
+ After login, both cookies are set automatically. `protect()` reads the access token from the cookie when no `Authorization` header is present.
500
388
 
501
- ```
502
- Body: { identifier, password }
503
- Returns: { accessToken, user: { id, identifier, roles } }
504
- Cookies: access_token=<jwt> (when config.accessCookie is set)
505
- refresh_token=<jwt> (httpOnly)
506
- Status: 200
507
- ```
389
+ ---
508
390
 
509
- #### `POST /refresh`
391
+ ## Endpoints
510
392
 
511
- ```
512
- Cookie: refresh_token=<token>
513
- Returns: { accessToken }
514
- Cookies: access_token=<new-jwt> (when config.accessCookie is set)
515
- refresh_token=<new-jwt> (rotated)
516
- Status: 200
517
- ```
393
+ `auth.router()` mounts these endpoints:
518
394
 
519
- #### `POST /logout`
395
+ | Method | Path | Auth | Description |
396
+ |---|---|---|---|
397
+ | `POST` | `/register` | — | Create a user (requires `X-Api-Key` when `apiKey` is set) |
398
+ | `POST` | `/login` | — | Authenticate, receive tokens |
399
+ | `POST` | `/refresh` | — | Rotate refresh token |
400
+ | `POST` | `/logout` | — | Invalidate current session |
401
+ | `POST` | `/logout-all` | ✓ | Invalidate all sessions |
402
+ | `GET` | `/me` | ✓ | Return authenticated user with all identifiers |
403
+ | `POST` | `/me/identifiers` | ✓ self | Add identifiers in bulk |
404
+ | `PUT` | `/me/identifiers` | ✓ self | Update identifiers in bulk |
405
+ | `DELETE` | `/me/identifiers` | ✓ self | Delete identifiers in bulk |
406
+ | `PATCH` | `/me/identifiers/primary` | ✓ self | Change primary identifier |
407
+ | `PATCH` | `/me/password` | ✓ self | Change password — revokes all sessions |
408
+ | `POST` | `/users/:userId/roles` | ✓ admin | Assign roles to user |
409
+ | `GET` | `/keys` | — | Public key in JWKS format (RS256 only) |
520
410
 
521
- ```
522
- Cookie: refresh_token=<token>
523
- Clears: access_token, refresh_token cookies
524
- Returns: null
525
- Status: 200
411
+ All responses use the envelope:
412
+ ```json
413
+ { "error": false, "statusCode": 200, "message": "...", "data": { ... } }
526
414
  ```
527
415
 
528
- #### `POST /logout-all`
416
+ ### Change Password
529
417
 
530
418
  ```
531
- Headers: Authorization: Bearer <accessToken> (or access_token cookie)
532
- Clears: access_token, refresh_token cookies
533
- Returns: null
534
- Status: 200
535
- ```
536
-
537
- #### `GET /me`
419
+ PATCH /me/password
420
+ Authorization: Bearer <token>
421
+ Content-Type: application/json
538
422
 
539
- ```
540
- Headers: Authorization: Bearer <accessToken> (or access_token cookie)
541
- Returns: { id, identifier, roles }
542
- Status: 200
423
+ { "currentPassword": "old-pass", "newPassword": "new-pass" }
543
424
  ```
544
425
 
545
- #### `POST /users/:userId/roles`
546
-
547
- ```
548
- Headers: Authorization: Bearer <accessToken> (must have admin role)
549
- Body: { roles: string[] }
550
- Returns: { user: { id, identifier, roles } }
551
- Status: 200
552
- ```
426
+ Changing the password revokes all existing sessions. The user must log in again after this call.
553
427
 
554
428
  ---
555
429
 
@@ -557,53 +431,40 @@ Status: 200
557
431
 
558
432
  ### `auth.protect()`
559
433
 
560
- Verifies the access token and injects `request.user`. Token is read from:
561
-
562
- 1. `Authorization: Bearer <token>` header
563
- 2. `access_token` cookie (when `config.accessCookie` is set)
564
-
565
- When the token has expired, a silent refresh is attempted automatically. On success the request proceeds normally; on failure HTTP 401 is returned.
434
+ Verifies the JWT and sets `req.user`. In server mode, also performs silent token refresh when the access token expires.
566
435
 
567
436
  ```typescript
568
- router.get('/dashboard', auth.protect(), (request, response) => {
569
- response.json(request.user); // { id, identifier, roles }
570
- });
437
+ app.get('/dashboard', auth.protect(), (req, res) => res.json(req.user));
438
+ // req.user: { id, identifier, identifierType, roles, identifiers? }
571
439
  ```
572
440
 
573
- Returns HTTP 401 if:
574
- - No token is present in header or cookie
575
- - The token signature is invalid
576
- - The token is expired **and** the refresh also fails
577
-
578
- ---
441
+ Token is read from `Authorization: Bearer <token>` header or `access_token` cookie.
579
442
 
580
443
  ### `auth.authorize(...roles)`
581
444
 
582
- Enforces role-based access. Must be used **after** `auth.protect()`.
445
+ Role-based access must follow `protect()`.
583
446
 
584
447
  ```typescript
585
- router.delete('/posts/:id', auth.protect(), auth.authorize('admin'), handler);
448
+ app.delete('/posts/:id', auth.protect(), auth.authorize('admin'), handler);
586
449
  ```
587
450
 
588
- ---
589
-
590
451
  ### `auth.permit(check | options)`
591
452
 
592
- Resource-level permission check. Must be used **after** `auth.protect()`. Return `true` to allow, `false` to deny.
453
+ Resource-level permission must follow `protect()`.
593
454
 
594
455
  ```typescript
595
456
  // Ownership check
596
- router.put('/users/:id', auth.protect(),
597
- auth.permit((req) => req.user!.id === req.params['id']),
457
+ app.put('/users/:id', auth.protect(),
458
+ auth.permit((req) => req.user!.id === req.params.id),
598
459
  handler,
599
460
  );
600
461
 
601
- // Role bypass + custom check
602
- router.delete('/posts/:id', auth.protect(),
462
+ // Role bypass + ownership check
463
+ app.delete('/posts/:id', auth.protect(),
603
464
  auth.permit({
604
465
  roles: ['admin'],
605
466
  check: async (req) => {
606
- const post = await db.post.findById(req.params['id']);
467
+ const post = await db.posts.findById(req.params.id);
607
468
  return post?.authorId === req.user!.id;
608
469
  },
609
470
  }),
@@ -613,140 +474,68 @@ router.delete('/posts/:id', auth.protect(),
613
474
 
614
475
  ---
615
476
 
616
- ### `createIdempotencyMiddleware(options?)`
617
-
618
- Makes mutating operations idempotent via `X-Idempotency-Key` header. See [Request Idempotency](#request-idempotency).
619
-
620
- ---
621
-
622
- ## Programmatic API
477
+ ## Token Utilities
623
478
 
624
- ### Token utilities
479
+ Available on `ServerAuthClient` only:
625
480
 
626
481
  ```typescript
627
- const accessToken = auth.signAccessToken({ id, identifier, roles });
628
- const user = auth.verifyAccessToken(accessToken); // throws SentriError if invalid
629
- const { sessionId } = auth.verifyRefreshToken(token); // throws SentriError if invalid
482
+ const auth = createAuth({ mode: 'server', ... });
483
+
484
+ // Sign
485
+ const accessToken = auth.signAccessToken({ id, identifier, identifierType, roles });
630
486
  const refreshToken = auth.signRefreshToken(sessionId);
631
- ```
632
487
 
633
- ### Password utilities
488
+ // Verify
489
+ const user = auth.verifyAccessToken(accessToken);
490
+ const { sessionId } = auth.verifyRefreshToken(refreshToken);
634
491
 
635
- ```typescript
636
- const hash = await auth.hashPassword('secret123');
492
+ // Password
493
+ const hash = await auth.hashPassword('secret123');
637
494
  const valid = await auth.verifyPassword('secret123', hash);
638
- ```
639
-
640
- ### Token extraction
641
-
642
- ```typescript
643
- // Read the raw access token from Authorization header or access_token cookie
644
- const token = auth.getCurrentAccessToken(request);
645
-
646
- // Or import and use directly with a config object
647
- import { getCurrentAccessToken } from 'sentri';
648
- const token = getCurrentAccessToken(request, config);
649
- ```
650
-
651
- ### Standalone `register`
652
-
653
- ```typescript
654
- import { register } from 'sentri';
655
495
 
656
- const result = await register(
657
- { identifier: 'alice@example.com', password: 'hunter2', roles: ['user'] },
658
- config,
659
- );
660
-
661
- if (result.success) {
662
- console.log(result.user.id);
663
- } else {
664
- console.error(result.error.code); // 'USER_ALREADY_EXISTS' | 'INVALID_ROLE'
665
- }
496
+ // Extract raw token from request
497
+ const token = auth.getCurrentAccessToken(req);
666
498
  ```
667
499
 
668
500
  ---
669
501
 
670
- ## Types
502
+ ## Error Handling
671
503
 
672
504
  ```typescript
673
- import type {
674
- AuthConfig,
675
- AuthClient,
676
- AuthAdapter,
677
- AuthHooks,
678
- AuthUser,
679
- AuthResult,
680
- RegisterResult,
681
- AssignRolesResult,
682
- RefreshResult,
683
- ApiResponse,
684
- RegisterInput,
685
- LoginInput,
686
- RouterHandlers,
687
- UserRecord,
688
- SessionRecord,
689
- CreateUserData,
690
- CookieConfig,
691
- AccessCookieConfig,
692
- IdempotencyOptions,
693
- PermitCheck,
694
- PermitOptions,
695
- SentriErrorCode,
696
- } from 'sentri';
697
-
698
- import {
699
- SentriError,
700
- AUTH_ERROR_STATUS,
701
- createAuth,
702
- register,
703
- createIdempotencyMiddleware,
704
- getCurrentAccessToken,
705
- } from 'sentri';
505
+ app.use('/auth', auth.router());
506
+ app.use('/api', apiRouter);
507
+ app.use(auth.errorHandler()); // must be last
706
508
  ```
707
509
 
708
- ---
709
-
710
- ## Error Handling
711
-
712
- All errors thrown by the library are instances of `SentriError` with a machine-readable `code` and an HTTP `statusCode`:
713
-
714
510
  | Code | HTTP | Meaning |
715
511
  |---|---|---|
716
512
  | `INVALID_CREDENTIALS` | 401 | Wrong identifier or password |
717
- | `USER_ALREADY_EXISTS` | 409 | Registration with duplicate identifier |
718
- | `USER_NOT_FOUND` | 404 | Operation on a non-existent user |
719
- | `TOKEN_EXPIRED` | 401 | JWT `exp` claim is in the past |
720
- | `TOKEN_INVALID` | 401 | JWT signature invalid or malformed |
721
- | `UNAUTHORIZED` | 401 | No valid access token, or session not found |
513
+ | `USER_NOT_FOUND` | 404 | User does not exist |
514
+ | `USER_ALREADY_EXISTS` | 409 | Duplicate identifier at registration |
515
+ | `IDENTIFIER_NOT_FOUND` | 404 | Referenced identifier ID does not exist or belongs to another user |
516
+ | `IDENTIFIER_ALREADY_EXISTS` | 409 | Identifier value already taken by another user |
517
+ | `TOKEN_EXPIRED` | 401 | JWT `exp` is in the past |
518
+ | `TOKEN_INVALID` | 401 | Bad signature or malformed JWT |
519
+ | `UNAUTHORIZED` | 401 | No valid token or session not found |
722
520
  | `FORBIDDEN` | 403 | Authenticated but missing required role |
723
- | `INVALID_ROLE` | 400 | Role name not in `validRoles` |
724
- | `VALIDATION_ERROR` | 400 | Missing or invalid input field |
725
- | `CONFIGURATION_ERROR` | 500 | Invalid `createAuth` configuration |
726
-
727
- ### `auth.errorHandler()`
728
-
729
- Mount **after all your routes**:
730
-
731
- ```typescript
732
- app.use('/auth', auth.router());
733
- app.use('/api', apiRouter);
734
- app.use(auth.errorHandler());
735
- ```
521
+ | `INVALID_ROLE` | 400 | Role not in `validRoles` |
522
+ | `VALIDATION_ERROR` | 400 | Missing or invalid input |
523
+ | `CONFIGURATION_ERROR` | 500 | Invalid `createAuth` config |
736
524
 
737
525
  ### Extending `SentriError`
738
526
 
739
527
  ```typescript
740
528
  import { SentriError } from 'sentri';
741
529
 
742
- export class NotFoundError extends SentriError {
530
+ class NotFoundError extends SentriError {
743
531
  constructor(resource: string) {
744
532
  super('NOT_FOUND', `${resource} not found`, 404);
745
533
  }
746
534
  }
747
535
 
536
+ // Caught automatically by auth.errorHandler()
748
537
  app.get('/items/:id', auth.protect(), async (req, res) => {
749
- const item = await db.items.findById(req.params['id']);
538
+ const item = await db.items.findById(req.params.id);
750
539
  if (!item) throw new NotFoundError('Item');
751
540
  res.json(item);
752
541
  });
@@ -754,39 +543,74 @@ app.get('/items/:id', auth.protect(), async (req, res) => {
754
543
 
755
544
  ---
756
545
 
757
- ## Migration Guide
546
+ ## Request Idempotency
758
547
 
759
- ### Breaking changes in 2.0.0
548
+ 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.
760
549
 
761
- | What changed | Action required |
762
- |---|---|
763
- | `protect()` no longer performs a DB session check per request | Access tokens are now stateless. Logout does **not** immediately invalidate a live access token — it expires after `accessExpiresIn`. Keep this value short (`'5m'`). |
764
- | `protect()` reads token from `access_token` cookie when `accessCookie` is configured | No action required if you use the `Authorization` header. Add `accessCookie` to config to enable cookie-based token delivery. |
765
- | `protect()` silently refreshes expired tokens | Clients should read the `X-New-Access-Token` response header to detect and store the new token. |
550
+ ### Via `createAuthServer()` (recommended)
551
+
552
+ ```typescript
553
+ const auth = createAuthServer({
554
+ validRoles: ['user', 'admin'] as const,
555
+ db: { connectionString: process.env.DATABASE_URL! },
556
+ redisUrl: process.env.REDIS_URL, // omit for in-memory cache
557
+ });
558
+
559
+ // Mount before your routes
560
+ app.use(auth.idempotencyMiddleware());
561
+
562
+ // Override TTL or methods
563
+ app.use(auth.idempotencyMiddleware({ ttl: 60_000 }));
564
+ ```
766
565
 
767
- ### New in 2.0.0
566
+ When `redisUrl` is set in server config, the middleware automatically uses Redis — no extra config needed.
768
567
 
769
- - **`accessCookie`** store the access token in a non-httpOnly cookie for SPA use.
770
- - **`auth.getCurrentAccessToken(req)`** / **`getCurrentAccessToken(req, config)`** — extract the token from header or cookie.
771
- - **`createIdempotencyMiddleware()`** — idempotent mutations via `X-Idempotency-Key`.
772
- - **`hooks`** (`onLogin`, `onFailedLogin`, `onLogout`) — fire-and-forget lifecycle callbacks for audit logs, rate limiting, cache invalidation.
773
- - **`isTokenRevoked`** — optional callback for Redis-backed immediate token revocation without giving up stateless JWTs.
774
- - **Minified bundle** — library is built with tsup, reducing load time and install footprint.
775
- - **Memoised config resolution** — `resolveConfig` is cached per config object; HMAC secret derivation is cached per secret string.
568
+ ### Standalone (without createAuthServer)
569
+
570
+ ```typescript
571
+ import { createIdempotencyMiddleware } from 'sentri';
572
+
573
+ // In-memory (single process)
574
+ app.use(createIdempotencyMiddleware({ ttl: 300_000 }));
575
+
576
+ // Redis (multi-process)
577
+ app.use(createIdempotencyMiddleware({ redisUrl: 'redis://localhost:6379' }));
578
+ ```
579
+
580
+ ### Options
581
+
582
+ | Option | Default | Description |
583
+ |---|---|---|
584
+ | `ttl` | `300_000` | Cache TTL in milliseconds |
585
+ | `header` | `'X-Idempotency-Key'` | Header name to read the key from |
586
+ | `methods` | `['POST','PUT','PATCH']` | HTTP methods to apply idempotency to |
587
+ | `maxSize` | `10_000` | Max in-memory entries (ignored when `redisUrl` is set) |
588
+ | `redisUrl` | — | Redis connection URL for multi-process cache |
589
+
590
+ ---
591
+
592
+ ## Migration Guide
776
593
 
777
- ### Breaking changes in 1.1.0
594
+ ### 4.0.0 Breaking Changes
778
595
 
779
596
  | What changed | Action required |
780
597
  |---|---|
781
- | `POST /signup` renamed to `POST /register` | Update all client-side calls |
782
- | `RouterHandlers.signup` renamed to `RouterHandlers.register` | Update config if you used a custom signup handler |
598
+ | `sentri_users` no longer has an `identifier` column | Drop and recreate tables — identities now live in `sentri_identifiers` |
599
+ | New `sentri_identifiers` table | Created automatically by `auth.migrate()` |
600
+ | `RegisterInput.identifier` → `RegisterInput.identifiers` (array) | Update register calls to pass `identifiers: [{ type, value }]` |
601
+ | `AuthUser` now includes `identifierType` | Update any code reading `req.user` to expect this new field |
602
+ | `PATCH /me/identifier` removed | Use `PUT /me/identifiers` for updates, `PATCH /me/identifiers/primary` for primary change |
603
+ | `changeIdentifier()` removed from `ServerAuthClient` | Use `bulkUpdateIdentifiers()` or `changePrimaryIdentifier()` |
604
+ | New error codes: `IDENTIFIER_NOT_FOUND`, `IDENTIFIER_ALREADY_EXISTS` | Handle these in your error handlers as needed |
783
605
 
784
- ### Breaking changes in 1.2.0
606
+ ### 3.0.0 Breaking Changes
785
607
 
786
608
  | What changed | Action required |
787
609
  |---|---|
788
- | `AuthError` renamed to `SentriError` | Replace all `import { AuthError }` with `import { SentriError }` |
789
- | `AuthErrorCode` renamed to `SentriErrorCode` | Replace all `AuthErrorCode` type references |
790
- | `SignupResult` renamed to `RegisterResult` | Replace type references |
791
- | `SignupInput` renamed to `RegisterInput` | Replace type references |
792
- | `signup` service renamed to `register` | Replace `import { signup }` with `import { register }` |
610
+ | `createAuth` now requires `mode: 'server'` or `mode: 'client'` | Add `mode: 'server'` to existing configs |
611
+ | `adapter` field removed Sentri owns the schema | Remove adapter, add `dialect` (Kysely Dialect) |
612
+ | `algorithm` now supports `'RS256' \| 'RS384' \| 'RS512'` | Optional HS256 still default |
613
+ | `AuthError` renamed to `SentriError` | Update imports: `import { SentriError } from 'sentri'` |
614
+ | `AUTH_ERROR_STATUS` renamed to `SENTRI_ERROR_STATUS` | Update references |
615
+ | `npx sentri generate` removed | Run `await auth.migrate()` at startup instead |
616
+ | Templates directory removed | No longer needed |