sentri 4.1.1 → 5.0.1
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 +462 -211
- package/dist/adapters/elysia/index.cjs +1 -0
- package/dist/adapters/elysia/index.d.cts +164 -0
- package/dist/adapters/elysia/index.d.ts +164 -0
- package/dist/adapters/elysia/index.js +1 -0
- package/dist/adapters/express/index.cjs +1 -0
- package/dist/adapters/express/index.d.cts +320 -0
- package/dist/adapters/express/index.d.ts +320 -0
- package/dist/adapters/express/index.js +1 -0
- package/dist/adapters/fastify/index.cjs +1 -0
- package/dist/adapters/fastify/index.d.cts +113 -0
- package/dist/adapters/fastify/index.d.ts +113 -0
- package/dist/adapters/fastify/index.js +1 -0
- package/dist/adapters/hono/index.cjs +1 -0
- package/dist/adapters/hono/index.d.cts +129 -0
- package/dist/adapters/hono/index.d.ts +129 -0
- package/dist/adapters/hono/index.js +1 -0
- package/dist/adapters/koa/index.cjs +1 -0
- package/dist/adapters/koa/index.d.cts +105 -0
- package/dist/adapters/koa/index.d.ts +105 -0
- package/dist/adapters/koa/index.js +1 -0
- package/dist/cli.cjs +529 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.js +449 -50
- package/dist/core/index.cjs +1 -0
- package/dist/core/index.d.cts +375 -0
- package/dist/core/index.d.ts +375 -0
- package/dist/core/index.js +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -671
- package/dist/index.js +1 -1
- package/package.json +78 -6
package/README.md
CHANGED
|
@@ -1,16 +1,35 @@
|
|
|
1
1
|
# sentri
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/sentri) [](LICENSE) [](https://github.com/rizzzdev/sentri/actions)
|
|
4
|
+
|
|
5
|
+
Auth and authorization library for Node.js — Express, Fastify, Hono, Elysia, and Koa. Supports two modes:
|
|
4
6
|
|
|
5
7
|
- **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
8
|
- **Client mode** — used by other apps to validate tokens issued by the auth server, without a database.
|
|
7
9
|
|
|
10
|
+
## Subpath exports
|
|
11
|
+
|
|
12
|
+
| Import | Description |
|
|
13
|
+
| ---------------- | ---------------------------------------------------------- |
|
|
14
|
+
| `sentri` | Full package re-export (backward compat) |
|
|
15
|
+
| `sentri/core` | Framework-agnostic types, `SentriError`, `SentriLogger` |
|
|
16
|
+
| `sentri/express` | Express adapter — `createAuthExpress`, middleware, router |
|
|
17
|
+
| `sentri/fastify` | Fastify adapter — `createAuthFastify`, preHandlers, plugin |
|
|
18
|
+
| `sentri/hono` | Hono adapter — `createAuthHono`, middleware, router |
|
|
19
|
+
| `sentri/elysia` | Elysia adapter — `createAuthElysia`, middleware, router |
|
|
20
|
+
| `sentri/koa` | Koa adapter — `createAuthKoa`, middleware, router |
|
|
21
|
+
|
|
8
22
|
---
|
|
9
23
|
|
|
10
24
|
## Table of Contents
|
|
11
25
|
|
|
12
26
|
- [Installation](#installation)
|
|
13
27
|
- [Quick Start](#quick-start)
|
|
28
|
+
- [Express](#express-server-mode-recommended)
|
|
29
|
+
- [Fastify](#fastify-server-mode)
|
|
30
|
+
- [Hono](#hono-server-mode)
|
|
31
|
+
- [Elysia](#elysia-server-mode)
|
|
32
|
+
- [Client mode](#client-mode-1)
|
|
14
33
|
- [Server Mode](#server-mode)
|
|
15
34
|
- [Client Mode](#client-mode)
|
|
16
35
|
- [SSO Flow](#sso-flow)
|
|
@@ -32,9 +51,15 @@ Auth and authorization library for Express. Supports two modes:
|
|
|
32
51
|
npm install sentri
|
|
33
52
|
```
|
|
34
53
|
|
|
35
|
-
`kysely`
|
|
54
|
+
`kysely` is bundled with sentri — no separate installation needed for Kysely. However, you must install the driver for your database of choice.
|
|
36
55
|
|
|
37
|
-
For
|
|
56
|
+
For PostgreSQL:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm install pg
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
For other databases:
|
|
38
63
|
|
|
39
64
|
```bash
|
|
40
65
|
# MySQL
|
|
@@ -44,21 +69,43 @@ npm install mysql2
|
|
|
44
69
|
npm install better-sqlite3
|
|
45
70
|
```
|
|
46
71
|
|
|
47
|
-
**Peer
|
|
72
|
+
**Peer dependencies** (install only what you use):
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Express
|
|
76
|
+
npm install express
|
|
77
|
+
|
|
78
|
+
# Fastify
|
|
79
|
+
npm install fastify @fastify/cookie
|
|
80
|
+
|
|
81
|
+
# Hono
|
|
82
|
+
npm install hono
|
|
83
|
+
|
|
84
|
+
# Elysia
|
|
85
|
+
npm install elysia
|
|
86
|
+
```
|
|
48
87
|
|
|
49
88
|
---
|
|
50
89
|
|
|
51
90
|
## Quick Start
|
|
52
91
|
|
|
53
|
-
###
|
|
92
|
+
### Express (server mode, recommended)
|
|
54
93
|
|
|
55
94
|
```typescript
|
|
56
|
-
import express from
|
|
57
|
-
import {
|
|
95
|
+
import express from "express";
|
|
96
|
+
import { createAuthExpress } from "sentri/express";
|
|
58
97
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
98
|
+
import { PostgresDialect } from "kysely";
|
|
99
|
+
import pg from "pg";
|
|
100
|
+
|
|
101
|
+
const { Pool } = pg;
|
|
102
|
+
|
|
103
|
+
const auth = createAuthExpress({
|
|
104
|
+
mode: "server",
|
|
105
|
+
validRoles: ["user", "admin"] as const,
|
|
106
|
+
dialect: new PostgresDialect({
|
|
107
|
+
pool: new Pool({ connectionString: process.env.DATABASE_URL! }),
|
|
108
|
+
}),
|
|
62
109
|
});
|
|
63
110
|
|
|
64
111
|
const app = express();
|
|
@@ -66,35 +113,190 @@ app.use(express.json());
|
|
|
66
113
|
app.use(auth.idempotencyMiddleware());
|
|
67
114
|
|
|
68
115
|
await auth.migrate();
|
|
69
|
-
app.use(
|
|
116
|
+
app.use("/auth", auth.router());
|
|
70
117
|
|
|
71
|
-
app.get(
|
|
118
|
+
app.get("/me", auth.protect(), (req, res) => res.json(req.user));
|
|
72
119
|
app.use(auth.errorHandler());
|
|
73
120
|
app.listen(3000);
|
|
74
121
|
```
|
|
75
122
|
|
|
76
123
|
`createAuthServer()` generates an RSA-2048 key pair at startup and enables `GET /keys` automatically.
|
|
77
124
|
|
|
125
|
+
For detailed Express docs see [src/adapters/express/README.md](src/adapters/express/README.md).
|
|
126
|
+
|
|
127
|
+
### Fastify (server mode)
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
import Fastify from "fastify";
|
|
131
|
+
import cookie from "@fastify/cookie";
|
|
132
|
+
import { createAuthFastify } from "sentri/fastify";
|
|
133
|
+
|
|
134
|
+
import { PostgresDialect } from "kysely";
|
|
135
|
+
import pg from "pg";
|
|
136
|
+
|
|
137
|
+
const { Pool } = pg;
|
|
138
|
+
|
|
139
|
+
const auth = createAuthFastify({
|
|
140
|
+
mode: "server",
|
|
141
|
+
validRoles: ["user", "admin"] as const,
|
|
142
|
+
dialect: new PostgresDialect({
|
|
143
|
+
pool: new Pool({ connectionString: process.env.DATABASE_URL! }),
|
|
144
|
+
}),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await auth.migrate();
|
|
148
|
+
|
|
149
|
+
const app = Fastify();
|
|
150
|
+
await app.register(cookie);
|
|
151
|
+
await app.register(auth.plugin(), { prefix: "/auth" });
|
|
152
|
+
app.setErrorHandler(auth.errorHandler());
|
|
153
|
+
|
|
154
|
+
app.get("/me", { preHandler: auth.protect() }, async (request, reply) => {
|
|
155
|
+
reply.send(request.user);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await app.listen({ port: 3000 });
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
For detailed Fastify docs see [src/adapters/fastify/README.md](src/adapters/fastify/README.md).
|
|
162
|
+
|
|
163
|
+
### Hono (server mode)
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { Hono } from "hono";
|
|
167
|
+
import { createAuthHono } from "sentri/hono";
|
|
168
|
+
import type { SentriHonoEnv } from "sentri/hono";
|
|
169
|
+
|
|
170
|
+
import { PostgresDialect } from "kysely";
|
|
171
|
+
import pg from "pg";
|
|
172
|
+
|
|
173
|
+
const { Pool } = pg;
|
|
174
|
+
|
|
175
|
+
const auth = createAuthHono({
|
|
176
|
+
mode: "server",
|
|
177
|
+
validRoles: ["user", "admin"] as const,
|
|
178
|
+
dialect: new PostgresDialect({
|
|
179
|
+
pool: new Pool({ connectionString: process.env.DATABASE_URL! }),
|
|
180
|
+
}),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await auth.migrate();
|
|
184
|
+
|
|
185
|
+
const app = new Hono<SentriHonoEnv>();
|
|
186
|
+
|
|
187
|
+
app.route("/auth", auth.router());
|
|
188
|
+
|
|
189
|
+
app.get("/me", auth.protect(), (c) => c.json(c.get("user")));
|
|
190
|
+
|
|
191
|
+
app.onError(auth.errorHandler());
|
|
192
|
+
|
|
193
|
+
export default app;
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`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.
|
|
197
|
+
|
|
198
|
+
For detailed Hono docs see [src/adapters/hono/README.md](src/adapters/hono/README.md).
|
|
199
|
+
|
|
200
|
+
### Elysia (server mode)
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
import { Elysia } from "elysia";
|
|
204
|
+
import { createAuthElysia } from "sentri/elysia";
|
|
205
|
+
|
|
206
|
+
import { PostgresDialect } from "kysely";
|
|
207
|
+
import pg from "pg";
|
|
208
|
+
|
|
209
|
+
const { Pool } = pg;
|
|
210
|
+
|
|
211
|
+
const auth = createAuthElysia({
|
|
212
|
+
mode: "server",
|
|
213
|
+
validRoles: ["user", "admin"] as const,
|
|
214
|
+
dialect: new PostgresDialect({
|
|
215
|
+
pool: new Pool({ connectionString: process.env.DATABASE_URL! }),
|
|
216
|
+
}),
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
await auth.migrate();
|
|
220
|
+
|
|
221
|
+
const app = new Elysia()
|
|
222
|
+
.onError(auth.errorHandler())
|
|
223
|
+
.group("/auth", (app) => app.use(auth.router()))
|
|
224
|
+
.use(auth.protect())
|
|
225
|
+
.get("/me", ({ user }) => user);
|
|
226
|
+
|
|
227
|
+
app.listen(3000);
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
For detailed Elysia docs see [src/adapters/elysia/README.md](src/adapters/elysia/README.md).
|
|
231
|
+
|
|
232
|
+
### Koa (server mode)
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import Koa from "koa";
|
|
236
|
+
import Router from "@koa/router";
|
|
237
|
+
import bodyParser from "koa-bodyparser";
|
|
238
|
+
import { createAuthKoa } from "sentri/koa";
|
|
239
|
+
|
|
240
|
+
const auth = createAuthKoa({
|
|
241
|
+
mode: "server",
|
|
242
|
+
validRoles: ["user", "admin"] as const,
|
|
243
|
+
db: { connectionString: process.env.DATABASE_URL! },
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
await auth.migrate();
|
|
247
|
+
|
|
248
|
+
const app = new Koa();
|
|
249
|
+
app.use(bodyParser());
|
|
250
|
+
app.use(auth.errorHandler());
|
|
251
|
+
|
|
252
|
+
const rootRouter = new Router();
|
|
253
|
+
rootRouter.use("/auth", auth.router().routes(), auth.router().allowedMethods());
|
|
254
|
+
|
|
255
|
+
rootRouter.get("/me", auth.protect(), (ctx) => {
|
|
256
|
+
ctx.body = ctx.state.user;
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
app.use(rootRouter.routes());
|
|
260
|
+
app.use(rootRouter.allowedMethods());
|
|
261
|
+
|
|
262
|
+
app.listen(3000);
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
For detailed Koa docs see [src/adapters/koa/README.md](src/adapters/koa/README.md).
|
|
266
|
+
|
|
267
|
+
### Client mode (any framework, any server)
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
import { createAuthExpress } from "sentri/express";
|
|
271
|
+
|
|
272
|
+
const auth = createAuthExpress({
|
|
273
|
+
mode: "client",
|
|
274
|
+
keyUri: "https://auth.myapp.com/auth/keys",
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
78
278
|
### Server Mode — Custom Dialect
|
|
79
279
|
|
|
80
280
|
```typescript
|
|
81
|
-
import express from
|
|
82
|
-
import { createAuth } from
|
|
83
|
-
import { PostgresDialect } from
|
|
84
|
-
import { Pool } from
|
|
281
|
+
import express from "express";
|
|
282
|
+
import { createAuth } from "sentri";
|
|
283
|
+
import { PostgresDialect } from "kysely"; // kysely is bundled, no install needed
|
|
284
|
+
import { Pool } from "pg"; // pg is bundled, no install needed
|
|
85
285
|
|
|
86
286
|
const auth = createAuth({
|
|
87
|
-
mode:
|
|
88
|
-
dialect: new PostgresDialect({
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
287
|
+
mode: "server",
|
|
288
|
+
dialect: new PostgresDialect({
|
|
289
|
+
pool: new Pool({ connectionString: process.env.DATABASE_URL }),
|
|
290
|
+
}),
|
|
291
|
+
secret: process.env.JWT_PRIVATE_KEY!, // RSA private key PEM (RS256) or plain string (HS256)
|
|
292
|
+
algorithm: "RS256",
|
|
293
|
+
validRoles: ["user", "admin"] as const,
|
|
92
294
|
});
|
|
93
295
|
|
|
94
296
|
const app = express();
|
|
95
297
|
app.use(express.json());
|
|
96
298
|
await auth.migrate();
|
|
97
|
-
app.use(
|
|
299
|
+
app.use("/auth", auth.router());
|
|
98
300
|
app.use(auth.errorHandler());
|
|
99
301
|
app.listen(3000);
|
|
100
302
|
```
|
|
@@ -102,17 +304,22 @@ app.listen(3000);
|
|
|
102
304
|
### Client Mode (Other Apps)
|
|
103
305
|
|
|
104
306
|
```typescript
|
|
105
|
-
import express from
|
|
106
|
-
import { createAuth } from
|
|
307
|
+
import express from "express";
|
|
308
|
+
import { createAuth } from "sentri";
|
|
107
309
|
|
|
108
310
|
const auth = createAuth({
|
|
109
|
-
mode:
|
|
110
|
-
keyUri:
|
|
311
|
+
mode: "client",
|
|
312
|
+
keyUri: "https://auth.myapp.com/auth/keys",
|
|
111
313
|
});
|
|
112
314
|
|
|
113
315
|
const app = express();
|
|
114
|
-
app.get(
|
|
115
|
-
app.get(
|
|
316
|
+
app.get("/products", auth.protect(), auth.authorize("admin"), handler);
|
|
317
|
+
app.get(
|
|
318
|
+
"/orders",
|
|
319
|
+
auth.protect(),
|
|
320
|
+
auth.permit((req) => req.user!.id === req.params.userId),
|
|
321
|
+
handler,
|
|
322
|
+
);
|
|
116
323
|
|
|
117
324
|
app.use(auth.errorHandler());
|
|
118
325
|
```
|
|
@@ -126,10 +333,10 @@ Server mode manages users, sessions, and tokens entirely within Sentri. The user
|
|
|
126
333
|
### `createAuthServer(options)` — PostgreSQL shortcut
|
|
127
334
|
|
|
128
335
|
```typescript
|
|
129
|
-
import { createAuthServer } from
|
|
336
|
+
import { createAuthServer } from "sentri/express";
|
|
130
337
|
|
|
131
338
|
const auth = createAuthServer({
|
|
132
|
-
validRoles: [
|
|
339
|
+
validRoles: ["user", "admin"] as const,
|
|
133
340
|
|
|
134
341
|
// Connection string
|
|
135
342
|
db: { connectionString: process.env.DATABASE_URL!, max: 10 },
|
|
@@ -138,37 +345,48 @@ const auth = createAuthServer({
|
|
|
138
345
|
// db: { host: 'localhost', port: 5432, database: 'mydb', user: 'app', password: 'secret' },
|
|
139
346
|
|
|
140
347
|
// Optional
|
|
141
|
-
accessExpiresIn:
|
|
142
|
-
refreshExpiresIn:
|
|
348
|
+
accessExpiresIn: "15m",
|
|
349
|
+
refreshExpiresIn: "7d",
|
|
143
350
|
saltRounds: 12,
|
|
144
351
|
apiKey: process.env.REGISTER_API_KEY,
|
|
145
|
-
redisUrl: process.env.REDIS_URL,
|
|
352
|
+
redisUrl: process.env.REDIS_URL, // enables Redis-backed idempotency cache
|
|
146
353
|
});
|
|
147
354
|
```
|
|
148
355
|
|
|
149
356
|
### `createAuth(config)` — Full config
|
|
150
357
|
|
|
151
358
|
```typescript
|
|
359
|
+
import { createAuth } from "sentri/express";
|
|
360
|
+
|
|
152
361
|
createAuth({
|
|
153
|
-
mode:
|
|
154
|
-
dialect,
|
|
155
|
-
secret: process.env.JWT_SECRET!,
|
|
156
|
-
validRoles: [
|
|
157
|
-
|
|
158
|
-
algorithm:
|
|
159
|
-
accessExpiresIn:
|
|
160
|
-
refreshExpiresIn:
|
|
161
|
-
saltRounds: 12,
|
|
162
|
-
apiKey: process.env.REGISTER_API_KEY,
|
|
163
|
-
redisUrl: process.env.REDIS_URL,
|
|
164
|
-
cookie: { secure: true },
|
|
165
|
-
accessCookie: { secure: true },
|
|
362
|
+
mode: "server",
|
|
363
|
+
dialect, // required — Kysely Dialect
|
|
364
|
+
secret: process.env.JWT_SECRET!, // required — RSA private key PEM (RS256) or string (HS256)
|
|
365
|
+
validRoles: ["user", "admin"] as const, // required
|
|
366
|
+
|
|
367
|
+
algorithm: "RS256", // default: 'HS256' | also: 'HS384','HS512','RS384','RS512'
|
|
368
|
+
accessExpiresIn: "15m", // default: '15m'
|
|
369
|
+
refreshExpiresIn: "7d", // default: '7d'
|
|
370
|
+
saltRounds: 12, // default: 12 (bcrypt rounds, 10–31)
|
|
371
|
+
apiKey: process.env.REGISTER_API_KEY, // restricts POST /register
|
|
372
|
+
redisUrl: process.env.REDIS_URL, // Redis URL for idempotency cache
|
|
373
|
+
cookie: { secure: true }, // httpOnly refresh token cookie
|
|
374
|
+
accessCookie: { secure: true }, // non-httpOnly access token cookie (SPA)
|
|
166
375
|
hooks: { onLogin, onFailedLogin, onLogout },
|
|
167
|
-
isTokenRevoked: async (sessionId) =>
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
376
|
+
isTokenRevoked: async (sessionId) =>
|
|
377
|
+
await redis.sismember("revoked", sessionId),
|
|
378
|
+
router: {
|
|
379
|
+
// override built-in service functions
|
|
380
|
+
login,
|
|
381
|
+
register,
|
|
382
|
+
refresh,
|
|
383
|
+
logout,
|
|
384
|
+
logoutAll,
|
|
385
|
+
assignRoles,
|
|
386
|
+
bulkCreateIdentifiers,
|
|
387
|
+
bulkUpdateIdentifiers,
|
|
388
|
+
bulkDeleteIdentifiers,
|
|
389
|
+
changePassword,
|
|
172
390
|
},
|
|
173
391
|
});
|
|
174
392
|
```
|
|
@@ -181,9 +399,10 @@ await auth.migrate();
|
|
|
181
399
|
```
|
|
182
400
|
|
|
183
401
|
Creates three tables:
|
|
402
|
+
|
|
184
403
|
- `sentri_users` — id, password_hash, roles (JSON), created_at
|
|
185
404
|
- `sentri_sessions` — id, user_id, expires_at, created_at
|
|
186
|
-
- `sentri_identifiers` — id, user_id, type, value (globally unique),
|
|
405
|
+
- `sentri_identifiers` — id, user_id, type, value (globally unique), created_at
|
|
187
406
|
|
|
188
407
|
---
|
|
189
408
|
|
|
@@ -193,9 +412,9 @@ Client mode has no database. It fetches the auth server's public key and validat
|
|
|
193
412
|
|
|
194
413
|
```typescript
|
|
195
414
|
createAuth({
|
|
196
|
-
mode:
|
|
197
|
-
keyUri:
|
|
198
|
-
validRoles: [
|
|
415
|
+
mode: "client",
|
|
416
|
+
keyUri: "https://auth.myapp.com/auth/keys", // required
|
|
417
|
+
validRoles: ["admin", "user"], // optional — TypeScript type safety only
|
|
199
418
|
});
|
|
200
419
|
```
|
|
201
420
|
|
|
@@ -231,11 +450,11 @@ Client apps point `keyUri` at this endpoint and receive the public key automatic
|
|
|
231
450
|
|
|
232
451
|
Each user can have multiple identifiers — email, username, phone number, or any custom type. All identifier values are **globally unique** regardless of type.
|
|
233
452
|
|
|
234
|
-
|
|
453
|
+
Login accepts any identifier value — Sentri searches all types automatically. No concept of "primary" exists; the JWT payload only contains `{ id, roles, sessionId }`.
|
|
235
454
|
|
|
236
455
|
### Registration
|
|
237
456
|
|
|
238
|
-
Provide at least one identifier.
|
|
457
|
+
Provide at least one identifier.
|
|
239
458
|
|
|
240
459
|
```
|
|
241
460
|
POST /register
|
|
@@ -252,6 +471,7 @@ Content-Type: application/json
|
|
|
252
471
|
```
|
|
253
472
|
|
|
254
473
|
**Response:**
|
|
474
|
+
|
|
255
475
|
```json
|
|
256
476
|
{
|
|
257
477
|
"error": false,
|
|
@@ -260,12 +480,10 @@ Content-Type: application/json
|
|
|
260
480
|
"data": {
|
|
261
481
|
"user": {
|
|
262
482
|
"id": "uuid",
|
|
263
|
-
"identifier": "rizz@example.com",
|
|
264
|
-
"identifierType": "email",
|
|
265
483
|
"roles": ["user"],
|
|
266
484
|
"identifiers": [
|
|
267
|
-
{ "id": "uuid-1", "type": "email",
|
|
268
|
-
{ "id": "uuid-2", "type": "username", "value": "rizz"
|
|
485
|
+
{ "id": "uuid-1", "type": "email", "value": "rizz@example.com" },
|
|
486
|
+
{ "id": "uuid-2", "type": "username", "value": "rizz" }
|
|
269
487
|
]
|
|
270
488
|
}
|
|
271
489
|
}
|
|
@@ -323,18 +541,6 @@ Content-Type: application/json
|
|
|
323
541
|
|
|
324
542
|
At least one identifier must remain after deletion.
|
|
325
543
|
|
|
326
|
-
### Change Primary Identifier
|
|
327
|
-
|
|
328
|
-
```
|
|
329
|
-
PATCH /me/identifiers/primary
|
|
330
|
-
Authorization: Bearer <token>
|
|
331
|
-
Content-Type: application/json
|
|
332
|
-
|
|
333
|
-
{ "id": "uuid-2" }
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
The new primary value will be embedded in the JWT on the next login or token refresh.
|
|
337
|
-
|
|
338
544
|
### Programmatic API
|
|
339
545
|
|
|
340
546
|
```typescript
|
|
@@ -357,9 +563,6 @@ await auth.bulkUpdateIdentifiers(userId, [{ id: 'uuid-2', type: 'username', valu
|
|
|
357
563
|
|
|
358
564
|
// Delete identifiers
|
|
359
565
|
await auth.bulkDeleteIdentifiers(userId, ['uuid-2']);
|
|
360
|
-
|
|
361
|
-
// Change primary
|
|
362
|
-
await auth.changePrimaryIdentifier(userId, 'uuid-3');
|
|
363
566
|
```
|
|
364
567
|
|
|
365
568
|
---
|
|
@@ -368,10 +571,10 @@ await auth.changePrimaryIdentifier(userId, 'uuid-3');
|
|
|
368
571
|
|
|
369
572
|
### `algorithm`
|
|
370
573
|
|
|
371
|
-
| Value
|
|
372
|
-
|
|
574
|
+
| Value | Type | Use case |
|
|
575
|
+
| --------- | ------------------- | ------------------------- |
|
|
373
576
|
| `'HS256'` | Symmetric (default) | Single app, shared secret |
|
|
374
|
-
| `'RS256'` | Asymmetric
|
|
577
|
+
| `'RS256'` | Asymmetric | SSO — enables `GET /keys` |
|
|
375
578
|
|
|
376
579
|
When using RS256, `secret` must be a valid RSA private key in PEM format. `createAuthServer()` generates the key pair automatically.
|
|
377
580
|
|
|
@@ -379,9 +582,9 @@ When using RS256, `secret` must be a valid RSA private key in PEM format. `creat
|
|
|
379
582
|
|
|
380
583
|
```typescript
|
|
381
584
|
createAuth({
|
|
382
|
-
mode:
|
|
383
|
-
cookie: { secure: true },
|
|
384
|
-
accessCookie: { secure: true },
|
|
585
|
+
mode: "server",
|
|
586
|
+
cookie: { secure: true }, // httpOnly refresh token
|
|
587
|
+
accessCookie: { secure: true }, // non-httpOnly access token (readable by JS)
|
|
385
588
|
});
|
|
386
589
|
```
|
|
387
590
|
|
|
@@ -393,23 +596,23 @@ After login, both cookies are set automatically. `protect()` reads the access to
|
|
|
393
596
|
|
|
394
597
|
`auth.router()` mounts these endpoints:
|
|
395
598
|
|
|
396
|
-
| Method
|
|
397
|
-
|
|
398
|
-
| `POST`
|
|
399
|
-
| `POST`
|
|
400
|
-
| `POST`
|
|
401
|
-
| `POST`
|
|
402
|
-
| `POST`
|
|
403
|
-
| `GET`
|
|
404
|
-
| `POST`
|
|
405
|
-
| `PUT`
|
|
406
|
-
| `DELETE` | `/me/identifiers`
|
|
407
|
-
| `PATCH`
|
|
408
|
-
| `
|
|
409
|
-
| `
|
|
410
|
-
| `GET` | `/keys` | — | Public key in JWKS format (RS256 only) |
|
|
599
|
+
| Method | Path | Auth | Description |
|
|
600
|
+
| -------- | ---------------------- | ------- | --------------------------------------------------------- |
|
|
601
|
+
| `POST` | `/register` | — | Create a user (requires `X-Api-Key` when `apiKey` is set) |
|
|
602
|
+
| `POST` | `/login` | — | Authenticate, receive tokens |
|
|
603
|
+
| `POST` | `/refresh` | — | Rotate refresh token |
|
|
604
|
+
| `POST` | `/logout` | — | Invalidate current session |
|
|
605
|
+
| `POST` | `/logout-all` | ✓ | Invalidate all sessions |
|
|
606
|
+
| `GET` | `/me` | ✓ | Return authenticated user with all identifiers |
|
|
607
|
+
| `POST` | `/me/identifiers` | ✓ self | Add identifiers in bulk |
|
|
608
|
+
| `PUT` | `/me/identifiers` | ✓ self | Update identifiers in bulk |
|
|
609
|
+
| `DELETE` | `/me/identifiers` | ✓ self | Delete identifiers in bulk |
|
|
610
|
+
| `PATCH` | `/me/password` | ✓ self | Change password — revokes all sessions |
|
|
611
|
+
| `POST` | `/users/:userId/roles` | ✓ admin | Assign roles to user |
|
|
612
|
+
| `GET` | `/keys` | — | Public key in JWKS format (RS256 only) |
|
|
411
613
|
|
|
412
614
|
All responses use the envelope:
|
|
615
|
+
|
|
413
616
|
```json
|
|
414
617
|
{ "error": false, "statusCode": 200, "message": "...", "data": { ... } }
|
|
415
618
|
```
|
|
@@ -432,45 +635,64 @@ Changing the password revokes all existing sessions. The user must log in again
|
|
|
432
635
|
|
|
433
636
|
### `auth.protect()`
|
|
434
637
|
|
|
435
|
-
Verifies the JWT and sets
|
|
638
|
+
Verifies the JWT and sets the user on the request context. In server mode, also performs silent token refresh when the access token expires.
|
|
639
|
+
|
|
640
|
+
Token is read from `Authorization: Bearer <token>` header or `access_token` cookie.
|
|
436
641
|
|
|
437
642
|
```typescript
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
643
|
+
// Express
|
|
644
|
+
app.get("/dashboard", auth.protect(), (req, res) => res.json(req.user));
|
|
645
|
+
// req.user: { id, roles }
|
|
646
|
+
|
|
647
|
+
// Fastify
|
|
648
|
+
app.get(
|
|
649
|
+
"/dashboard",
|
|
650
|
+
{ preHandler: auth.protect() },
|
|
651
|
+
async (request) => request.user,
|
|
652
|
+
);
|
|
441
653
|
|
|
442
|
-
|
|
654
|
+
// Hono
|
|
655
|
+
app.get("/dashboard", auth.protect(), (c) => c.json(c.get("user")));
|
|
656
|
+
```
|
|
443
657
|
|
|
444
658
|
### `auth.authorize(...roles)`
|
|
445
659
|
|
|
446
660
|
Role-based access — must follow `protect()`.
|
|
447
661
|
|
|
448
662
|
```typescript
|
|
449
|
-
|
|
663
|
+
// Express / Fastify preHandler / Hono — same API
|
|
664
|
+
app.delete("/posts/:id", auth.protect(), auth.authorize("admin"), handler);
|
|
450
665
|
```
|
|
451
666
|
|
|
452
667
|
### `auth.permit(check | options)`
|
|
453
668
|
|
|
454
|
-
Resource-level permission — must follow `protect()`.
|
|
669
|
+
Resource-level permission — must follow `protect()`. The check function receives the request context object of each framework.
|
|
455
670
|
|
|
456
671
|
```typescript
|
|
457
|
-
//
|
|
458
|
-
app.put(
|
|
672
|
+
// Express
|
|
673
|
+
app.put(
|
|
674
|
+
"/users/:id",
|
|
675
|
+
auth.protect(),
|
|
459
676
|
auth.permit((req) => req.user!.id === req.params.id),
|
|
460
677
|
handler,
|
|
461
678
|
);
|
|
462
679
|
|
|
463
|
-
//
|
|
464
|
-
app.
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const post = await db.posts.findById(req.params.id);
|
|
469
|
-
return post?.authorId === req.user!.id;
|
|
470
|
-
},
|
|
471
|
-
}),
|
|
680
|
+
// Hono
|
|
681
|
+
app.put(
|
|
682
|
+
"/users/:id",
|
|
683
|
+
auth.protect(),
|
|
684
|
+
auth.permit((c) => c.get("user")!.id === c.req.param("id")),
|
|
472
685
|
handler,
|
|
473
686
|
);
|
|
687
|
+
|
|
688
|
+
// Role bypass + ownership check
|
|
689
|
+
auth.permit({
|
|
690
|
+
roles: ["admin"],
|
|
691
|
+
check: async (req) => {
|
|
692
|
+
const post = await db.posts.findById(req.params.id);
|
|
693
|
+
return post?.authorId === req.user!.id;
|
|
694
|
+
},
|
|
695
|
+
});
|
|
474
696
|
```
|
|
475
697
|
|
|
476
698
|
---
|
|
@@ -483,7 +705,7 @@ Available on `ServerAuthClient` only:
|
|
|
483
705
|
const auth = createAuth({ mode: 'server', ... });
|
|
484
706
|
|
|
485
707
|
// Sign
|
|
486
|
-
const accessToken = auth.signAccessToken({ id,
|
|
708
|
+
const accessToken = auth.signAccessToken({ id, roles });
|
|
487
709
|
const refreshToken = auth.signRefreshToken(sessionId);
|
|
488
710
|
|
|
489
711
|
// Verify
|
|
@@ -503,41 +725,49 @@ const token = auth.getCurrentAccessToken(req);
|
|
|
503
725
|
## Error Handling
|
|
504
726
|
|
|
505
727
|
```typescript
|
|
506
|
-
|
|
507
|
-
app.use(
|
|
508
|
-
app.use(
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
|
520
|
-
|
|
|
521
|
-
| `
|
|
522
|
-
| `
|
|
523
|
-
| `
|
|
524
|
-
| `
|
|
728
|
+
// Express — must be last
|
|
729
|
+
app.use("/auth", auth.router());
|
|
730
|
+
app.use("/api", apiRouter);
|
|
731
|
+
app.use(auth.errorHandler());
|
|
732
|
+
|
|
733
|
+
// Fastify
|
|
734
|
+
app.setErrorHandler(auth.errorHandler());
|
|
735
|
+
|
|
736
|
+
// Hono — mount via onError
|
|
737
|
+
app.route("/auth", auth.router());
|
|
738
|
+
app.onError(auth.errorHandler());
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
| Code | HTTP | Meaning |
|
|
742
|
+
| --------------------------- | ---- | ------------------------------------------------------------------ |
|
|
743
|
+
| `INVALID_CREDENTIALS` | 401 | Wrong identifier or password |
|
|
744
|
+
| `USER_NOT_FOUND` | 404 | User does not exist |
|
|
745
|
+
| `USER_ALREADY_EXISTS` | 409 | Duplicate identifier at registration |
|
|
746
|
+
| `IDENTIFIER_NOT_FOUND` | 404 | Referenced identifier ID does not exist or belongs to another user |
|
|
747
|
+
| `IDENTIFIER_ALREADY_EXISTS` | 409 | Identifier value already taken by another user |
|
|
748
|
+
| `TOKEN_EXPIRED` | 401 | JWT `exp` is in the past |
|
|
749
|
+
| `TOKEN_INVALID` | 401 | Bad signature or malformed JWT |
|
|
750
|
+
| `UNAUTHORIZED` | 401 | No valid token or session not found |
|
|
751
|
+
| `FORBIDDEN` | 403 | Authenticated but missing required role |
|
|
752
|
+
| `INVALID_ROLE` | 400 | Role not in `validRoles` |
|
|
753
|
+
| `VALIDATION_ERROR` | 400 | Missing or invalid input |
|
|
754
|
+
| `CONFIGURATION_ERROR` | 500 | Invalid `createAuth` config |
|
|
525
755
|
|
|
526
756
|
### Extending `SentriError`
|
|
527
757
|
|
|
528
758
|
```typescript
|
|
529
|
-
import { SentriError } from
|
|
759
|
+
import { SentriError } from "sentri";
|
|
530
760
|
|
|
531
761
|
class NotFoundError extends SentriError {
|
|
532
762
|
constructor(resource: string) {
|
|
533
|
-
super(
|
|
763
|
+
super("NOT_FOUND", `${resource} not found`, 404);
|
|
534
764
|
}
|
|
535
765
|
}
|
|
536
766
|
|
|
537
767
|
// Caught automatically by auth.errorHandler()
|
|
538
|
-
app.get(
|
|
768
|
+
app.get("/items/:id", auth.protect(), async (req, res) => {
|
|
539
769
|
const item = await db.items.findById(req.params.id);
|
|
540
|
-
if (!item) throw new NotFoundError(
|
|
770
|
+
if (!item) throw new NotFoundError("Item");
|
|
541
771
|
res.json(item);
|
|
542
772
|
});
|
|
543
773
|
```
|
|
@@ -552,9 +782,9 @@ Repeat requests with the same `X-Idempotency-Key` header receive the cached resp
|
|
|
552
782
|
|
|
553
783
|
```typescript
|
|
554
784
|
const auth = createAuthServer({
|
|
555
|
-
validRoles: [
|
|
785
|
+
validRoles: ["user", "admin"] as const,
|
|
556
786
|
db: { connectionString: process.env.DATABASE_URL! },
|
|
557
|
-
redisUrl: process.env.REDIS_URL,
|
|
787
|
+
redisUrl: process.env.REDIS_URL, // omit for in-memory cache
|
|
558
788
|
});
|
|
559
789
|
|
|
560
790
|
// Mount before your routes
|
|
@@ -569,24 +799,24 @@ When `redisUrl` is set in server config, the middleware automatically uses Redis
|
|
|
569
799
|
### Standalone (without createAuthServer)
|
|
570
800
|
|
|
571
801
|
```typescript
|
|
572
|
-
import { createIdempotencyMiddleware } from
|
|
802
|
+
import { createIdempotencyMiddleware } from "sentri";
|
|
573
803
|
|
|
574
804
|
// In-memory (single process)
|
|
575
805
|
app.use(createIdempotencyMiddleware({ ttl: 300_000 }));
|
|
576
806
|
|
|
577
807
|
// Redis (multi-process)
|
|
578
|
-
app.use(createIdempotencyMiddleware({ redisUrl:
|
|
808
|
+
app.use(createIdempotencyMiddleware({ redisUrl: "redis://localhost:6379" }));
|
|
579
809
|
```
|
|
580
810
|
|
|
581
811
|
### Options
|
|
582
812
|
|
|
583
|
-
| Option
|
|
584
|
-
|
|
585
|
-
| `ttl`
|
|
586
|
-
| `header`
|
|
587
|
-
| `methods`
|
|
588
|
-
| `maxSize`
|
|
589
|
-
| `redisUrl` | —
|
|
813
|
+
| Option | Default | Description |
|
|
814
|
+
| ---------- | ------------------------ | ------------------------------------------------------ |
|
|
815
|
+
| `ttl` | `300_000` | Cache TTL in milliseconds |
|
|
816
|
+
| `header` | `'X-Idempotency-Key'` | Header name to read the key from |
|
|
817
|
+
| `methods` | `['POST','PUT','PATCH']` | HTTP methods to apply idempotency to |
|
|
818
|
+
| `maxSize` | `10_000` | Max in-memory entries (ignored when `redisUrl` is set) |
|
|
819
|
+
| `redisUrl` | — | Redis connection URL for multi-process cache |
|
|
590
820
|
|
|
591
821
|
---
|
|
592
822
|
|
|
@@ -599,22 +829,24 @@ Sentri produces structured JSON log entries for every auth event. Logging is **o
|
|
|
599
829
|
Pass any object that implements `{ info, warn, error }` via the `logger` field in your config.
|
|
600
830
|
|
|
601
831
|
**pino** (recommended for production):
|
|
832
|
+
|
|
602
833
|
```typescript
|
|
603
|
-
import pino from
|
|
834
|
+
import pino from "pino";
|
|
604
835
|
|
|
605
836
|
const auth = createAuth({
|
|
606
|
-
mode:
|
|
837
|
+
mode: "server",
|
|
607
838
|
// ...other config
|
|
608
839
|
logger: pino(),
|
|
609
840
|
});
|
|
610
841
|
```
|
|
611
842
|
|
|
612
843
|
**winston**:
|
|
844
|
+
|
|
613
845
|
```typescript
|
|
614
|
-
import winston from
|
|
846
|
+
import winston from "winston";
|
|
615
847
|
|
|
616
848
|
const auth = createAuth({
|
|
617
|
-
mode:
|
|
849
|
+
mode: "server",
|
|
618
850
|
// ...other config
|
|
619
851
|
logger: winston.createLogger({
|
|
620
852
|
transports: [new winston.transports.Console()],
|
|
@@ -623,19 +855,21 @@ const auth = createAuth({
|
|
|
623
855
|
```
|
|
624
856
|
|
|
625
857
|
**console** (zero setup, good for development):
|
|
858
|
+
|
|
626
859
|
```typescript
|
|
627
860
|
const auth = createAuth({
|
|
628
|
-
mode:
|
|
861
|
+
mode: "server",
|
|
629
862
|
// ...other config
|
|
630
863
|
logger: console,
|
|
631
864
|
});
|
|
632
865
|
```
|
|
633
866
|
|
|
634
867
|
Works identically in **client mode**:
|
|
868
|
+
|
|
635
869
|
```typescript
|
|
636
870
|
const auth = createAuth({
|
|
637
|
-
mode:
|
|
638
|
-
keyUri:
|
|
871
|
+
mode: "client",
|
|
872
|
+
keyUri: "https://auth.myapp.com/auth/keys",
|
|
639
873
|
logger: pino(),
|
|
640
874
|
});
|
|
641
875
|
```
|
|
@@ -648,7 +882,7 @@ By default every log entry contains `"service": "sentri"`. Override it with `log
|
|
|
648
882
|
const auth = createAuth({
|
|
649
883
|
// ...
|
|
650
884
|
logger: pino(),
|
|
651
|
-
loggerService:
|
|
885
|
+
loggerService: "auth-service",
|
|
652
886
|
});
|
|
653
887
|
```
|
|
654
888
|
|
|
@@ -657,17 +891,18 @@ const auth = createAuth({
|
|
|
657
891
|
If you mount a request-ID middleware **before** Sentri, the `requestId` is automatically included in every log entry for that request:
|
|
658
892
|
|
|
659
893
|
```typescript
|
|
660
|
-
import { randomUUID } from
|
|
894
|
+
import { randomUUID } from "crypto";
|
|
661
895
|
|
|
662
896
|
app.use((req, _res, next) => {
|
|
663
897
|
req.requestId = randomUUID();
|
|
664
898
|
next();
|
|
665
899
|
});
|
|
666
900
|
|
|
667
|
-
app.use(
|
|
901
|
+
app.use("/auth", auth.router());
|
|
668
902
|
```
|
|
669
903
|
|
|
670
904
|
Sample pino output:
|
|
905
|
+
|
|
671
906
|
```json
|
|
672
907
|
{"level":30,"time":1719484800000,"service":"sentri","event":"auth.login.success","userId":"usr_abc","duration_ms":42,"requestId":"d4e5f6"}
|
|
673
908
|
{"level":40,"time":1719484801000,"service":"sentri","event":"auth.authorize.denied","userId":"usr_abc","userRoles":["user"],"requiredRoles":["admin"],"requestId":"d4e5f7"}
|
|
@@ -675,47 +910,52 @@ Sample pino output:
|
|
|
675
910
|
|
|
676
911
|
### Log events
|
|
677
912
|
|
|
678
|
-
| Event
|
|
679
|
-
|
|
680
|
-
| `auth.protect.success`
|
|
681
|
-
| `auth.protect.failure`
|
|
682
|
-
| `auth.protect.token_revoked`
|
|
683
|
-
| `auth.protect.auto_refresh`
|
|
684
|
-
| `auth.authorize.passed`
|
|
685
|
-
| `auth.authorize.denied`
|
|
686
|
-
| `auth.authorize.unauthenticated` | warn
|
|
687
|
-
| `auth.permit.passed`
|
|
688
|
-
| `auth.permit.denied`
|
|
689
|
-
| `auth.permit.role_bypass`
|
|
690
|
-
| `auth.permit.unauthenticated`
|
|
691
|
-
| `auth.register.success`
|
|
692
|
-
| `auth.register.failure`
|
|
693
|
-
| `auth.login.success`
|
|
694
|
-
| `auth.login.failure`
|
|
695
|
-
| `auth.refresh.success`
|
|
696
|
-
| `auth.refresh.failure`
|
|
697
|
-
| `auth.logout`
|
|
698
|
-
| `auth.logout_all`
|
|
699
|
-
| `auth.password.changed`
|
|
700
|
-
| `auth.password.change_failure`
|
|
701
|
-
| `auth.roles.assigned`
|
|
702
|
-
| `auth.roles.assign_failure`
|
|
703
|
-
| `auth.identifiers.created`
|
|
704
|
-
| `auth.identifiers.updated`
|
|
705
|
-
| `auth.identifiers.deleted`
|
|
706
|
-
| `auth.identifiers.primary_changed` | info | router | `userId`, `duration_ms` |
|
|
913
|
+
| Event | Level | Emitted by | Key fields |
|
|
914
|
+
| -------------------------------- | ----- | ------------- | ------------------------------------------ |
|
|
915
|
+
| `auth.protect.success` | info | `protect()` | `userId`, `mode` |
|
|
916
|
+
| `auth.protect.failure` | warn | `protect()` | `errorCode`, `mode` |
|
|
917
|
+
| `auth.protect.token_revoked` | warn | `protect()` | `userId`, `mode` |
|
|
918
|
+
| `auth.protect.auto_refresh` | info | `protect()` | `userId`, `mode` |
|
|
919
|
+
| `auth.authorize.passed` | info | `authorize()` | `userId`, `userRoles`, `requiredRoles` |
|
|
920
|
+
| `auth.authorize.denied` | warn | `authorize()` | `userId`, `userRoles`, `requiredRoles` |
|
|
921
|
+
| `auth.authorize.unauthenticated` | warn | `authorize()` | `requiredRoles` |
|
|
922
|
+
| `auth.permit.passed` | info | `permit()` | `userId` |
|
|
923
|
+
| `auth.permit.denied` | warn | `permit()` | `userId` |
|
|
924
|
+
| `auth.permit.role_bypass` | info | `permit()` | `userId`, `bypassedByRole` |
|
|
925
|
+
| `auth.permit.unauthenticated` | warn | `permit()` | — |
|
|
926
|
+
| `auth.register.success` | info | router | `userId`, `duration_ms` |
|
|
927
|
+
| `auth.register.failure` | warn | router | `errorCode`, `duration_ms` |
|
|
928
|
+
| `auth.login.success` | info | router | `userId`, `duration_ms` |
|
|
929
|
+
| `auth.login.failure` | warn | router | `errorCode`, `duration_ms` |
|
|
930
|
+
| `auth.refresh.success` | info | router | `userId`, `duration_ms` |
|
|
931
|
+
| `auth.refresh.failure` | warn | router | `errorCode`, `duration_ms` |
|
|
932
|
+
| `auth.logout` | info | router | `duration_ms` |
|
|
933
|
+
| `auth.logout_all` | info | router | `userId`, `duration_ms` |
|
|
934
|
+
| `auth.password.changed` | info | router | `userId`, `duration_ms` |
|
|
935
|
+
| `auth.password.change_failure` | warn | router | `userId`, `errorCode`, `duration_ms` |
|
|
936
|
+
| `auth.roles.assigned` | info | router | `targetUserId`, `roles`, `duration_ms` |
|
|
937
|
+
| `auth.roles.assign_failure` | warn | router | `targetUserId`, `errorCode`, `duration_ms` |
|
|
938
|
+
| `auth.identifiers.created` | info | router | `userId`, `count`, `duration_ms` |
|
|
939
|
+
| `auth.identifiers.updated` | info | router | `userId`, `count`, `duration_ms` |
|
|
940
|
+
| `auth.identifiers.deleted` | info | router | `userId`, `duration_ms` |
|
|
707
941
|
|
|
708
942
|
All entries include `service` (configurable via `loggerService`) and `requestId` when available.
|
|
709
943
|
|
|
710
944
|
### `SentriLogger` interface
|
|
711
945
|
|
|
712
946
|
```typescript
|
|
713
|
-
import type { SentriLogger } from
|
|
947
|
+
import type { SentriLogger } from "sentri";
|
|
714
948
|
|
|
715
949
|
const myLogger: SentriLogger = {
|
|
716
|
-
info(data: Record<string, unknown>) {
|
|
717
|
-
|
|
718
|
-
|
|
950
|
+
info(data: Record<string, unknown>) {
|
|
951
|
+
/* ... */
|
|
952
|
+
},
|
|
953
|
+
warn(data: Record<string, unknown>) {
|
|
954
|
+
/* ... */
|
|
955
|
+
},
|
|
956
|
+
error(data: Record<string, unknown>) {
|
|
957
|
+
/* ... */
|
|
958
|
+
},
|
|
719
959
|
};
|
|
720
960
|
```
|
|
721
961
|
|
|
@@ -723,26 +963,37 @@ const myLogger: SentriLogger = {
|
|
|
723
963
|
|
|
724
964
|
## Migration Guide
|
|
725
965
|
|
|
966
|
+
### 5.0.0 Breaking Changes
|
|
967
|
+
|
|
968
|
+
| What changed | Action required |
|
|
969
|
+
| -------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
970
|
+
| `is_primary` column removed from `sentri_identifiers` | Drop and recreate tables — run `auth.migrate()` after |
|
|
971
|
+
| `AuthUser` no longer has `identifier` / `identifierType` | Update any code reading `req.user` — shape is now `{ id, roles }` |
|
|
972
|
+
| JWT payload no longer includes `identifier` / `identifierType` | Any code decoding the token directly must drop these fields |
|
|
973
|
+
| `PATCH /me/identifiers/primary` endpoint removed | No replacement — the concept of primary is gone |
|
|
974
|
+
| `changePrimaryIdentifier()` removed from `ServerAuthClient` | Delete call sites |
|
|
975
|
+
| `ChangePrimaryResult` type removed | Delete references from type imports |
|
|
976
|
+
| `bulkDeleteIdentifiers` guard changed | Old: "cannot delete primary". New: "must keep at least one identifier" |
|
|
977
|
+
|
|
726
978
|
### 4.0.0 Breaking Changes
|
|
727
979
|
|
|
728
|
-
| What changed
|
|
729
|
-
|
|
730
|
-
| `sentri_users` no longer has an `identifier` column
|
|
731
|
-
| New `sentri_identifiers` table
|
|
732
|
-
| `RegisterInput.identifier` → `RegisterInput.identifiers` (array)
|
|
733
|
-
| `
|
|
734
|
-
| `
|
|
735
|
-
|
|
|
736
|
-
| New error codes: `IDENTIFIER_NOT_FOUND`, `IDENTIFIER_ALREADY_EXISTS` | Handle these in your error handlers as needed |
|
|
980
|
+
| What changed | Action required |
|
|
981
|
+
| -------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
982
|
+
| `sentri_users` no longer has an `identifier` column | Drop and recreate tables — identities now live in `sentri_identifiers` |
|
|
983
|
+
| New `sentri_identifiers` table | Created automatically by `auth.migrate()` |
|
|
984
|
+
| `RegisterInput.identifier` → `RegisterInput.identifiers` (array) | Update register calls to pass `identifiers: [{ type, value }]` |
|
|
985
|
+
| `PATCH /me/identifier` removed | Use `PUT /me/identifiers` for updates |
|
|
986
|
+
| `changeIdentifier()` removed from `ServerAuthClient` | Use `bulkUpdateIdentifiers()` |
|
|
987
|
+
| New error codes: `IDENTIFIER_NOT_FOUND`, `IDENTIFIER_ALREADY_EXISTS` | Handle these in your error handlers as needed |
|
|
737
988
|
|
|
738
989
|
### 3.0.0 Breaking Changes
|
|
739
990
|
|
|
740
|
-
| What changed
|
|
741
|
-
|
|
742
|
-
| `createAuth` now requires `mode: 'server'` or `mode: 'client'` | Add `mode: 'server'` to existing configs
|
|
743
|
-
| `adapter` field removed — Sentri owns the schema
|
|
744
|
-
| `algorithm` now supports `'RS256' \| 'RS384' \| 'RS512'`
|
|
745
|
-
| `AuthError` renamed to `SentriError`
|
|
746
|
-
| `AUTH_ERROR_STATUS` renamed to `SENTRI_ERROR_STATUS`
|
|
747
|
-
| `npx sentri generate` removed
|
|
748
|
-
| Templates directory removed
|
|
991
|
+
| What changed | Action required |
|
|
992
|
+
| -------------------------------------------------------------- | ------------------------------------------------------ |
|
|
993
|
+
| `createAuth` now requires `mode: 'server'` or `mode: 'client'` | Add `mode: 'server'` to existing configs |
|
|
994
|
+
| `adapter` field removed — Sentri owns the schema | Remove adapter, add `dialect` (Kysely Dialect) |
|
|
995
|
+
| `algorithm` now supports `'RS256' \| 'RS384' \| 'RS512'` | Optional — HS256 still default |
|
|
996
|
+
| `AuthError` renamed to `SentriError` | Update imports: `import { SentriError } from 'sentri'` |
|
|
997
|
+
| `AUTH_ERROR_STATUS` renamed to `SENTRI_ERROR_STATUS` | Update references |
|
|
998
|
+
| `npx sentri generate` removed | Run `await auth.migrate()` at startup instead |
|
|
999
|
+
| Templates directory removed | No longer needed |
|