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 +372 -548
- package/dist/cli.js +112 -13
- package/dist/index.d.ts +303 -750
- package/dist/index.js +1 -1
- package/package.json +7 -12
- package/templates/drizzle/adapter.ts +0 -154
- package/templates/drizzle/auth.ts +0 -125
- package/templates/drizzle/schema.ts +0 -47
- package/templates/prisma/adapter.ts +0 -122
- package/templates/prisma/auth.ts +0 -128
- package/templates/prisma/schema.prisma +0 -56
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Dialect } from 'kysely';
|
|
1
2
|
import { Request, ErrorRequestHandler, RequestHandler, Router } from 'express';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -6,6 +7,8 @@ import { Request, ErrorRequestHandler, RequestHandler, Router } from 'express';
|
|
|
6
7
|
* - `INVALID_CREDENTIALS` — identifier or password did not match (intentionally vague to prevent user enumeration)
|
|
7
8
|
* - `USER_NOT_FOUND` — an operation required a user that does not exist
|
|
8
9
|
* - `USER_ALREADY_EXISTS` — registration was attempted with an identifier already in the database
|
|
10
|
+
* - `IDENTIFIER_NOT_FOUND` — a referenced identifier ID does not exist or does not belong to the user
|
|
11
|
+
* - `IDENTIFIER_ALREADY_EXISTS` — the identifier value is already taken by another user
|
|
9
12
|
* - `TOKEN_EXPIRED` — the JWT was valid but its `exp` claim is in the past
|
|
10
13
|
* - `TOKEN_INVALID` — the JWT could not be verified (bad signature, malformed, wrong type)
|
|
11
14
|
* - `FORBIDDEN` — the user is authenticated but lacks the required role
|
|
@@ -17,14 +20,14 @@ import { Request, ErrorRequestHandler, RequestHandler, Router } from 'express';
|
|
|
17
20
|
* When you extend {@link SentriError} for your own error types you can use any
|
|
18
21
|
* string as `code` — it does not need to be one of these built-in values.
|
|
19
22
|
*/
|
|
20
|
-
type SentriErrorCode = 'INVALID_CREDENTIALS' | 'USER_NOT_FOUND' | 'USER_ALREADY_EXISTS' | 'TOKEN_EXPIRED' | 'TOKEN_INVALID' | 'FORBIDDEN' | 'UNAUTHORIZED' | 'INVALID_ROLE' | 'VALIDATION_ERROR' | 'CONFIGURATION_ERROR';
|
|
23
|
+
type SentriErrorCode = 'INVALID_CREDENTIALS' | 'USER_NOT_FOUND' | 'USER_ALREADY_EXISTS' | 'IDENTIFIER_NOT_FOUND' | 'IDENTIFIER_ALREADY_EXISTS' | 'TOKEN_EXPIRED' | 'TOKEN_INVALID' | 'FORBIDDEN' | 'UNAUTHORIZED' | 'INVALID_ROLE' | 'VALIDATION_ERROR' | 'CONFIGURATION_ERROR';
|
|
21
24
|
/**
|
|
22
25
|
* Default HTTP status codes for built-in error codes.
|
|
23
|
-
* Custom codes
|
|
26
|
+
* Custom codes not in this map default to 500.
|
|
24
27
|
*
|
|
25
28
|
* @internal
|
|
26
29
|
*/
|
|
27
|
-
declare const
|
|
30
|
+
declare const SENTRI_ERROR_STATUS: Record<string, number>;
|
|
28
31
|
/**
|
|
29
32
|
* Base error class for all authentication and authorization failures in sentri.
|
|
30
33
|
*
|
|
@@ -49,14 +52,12 @@ declare const AUTH_ERROR_STATUS: Record<string, number>;
|
|
|
49
52
|
* ```typescript
|
|
50
53
|
* import { SentriError } from 'sentri';
|
|
51
54
|
*
|
|
52
|
-
* // Domain error with a custom code and explicit HTTP status
|
|
53
55
|
* export class PaymentError extends SentriError {
|
|
54
56
|
* constructor(message: string) {
|
|
55
57
|
* super('PAYMENT_FAILED', message, 402);
|
|
56
58
|
* }
|
|
57
59
|
* }
|
|
58
60
|
*
|
|
59
|
-
* // Throw it anywhere in your routes — auth.errorHandler() catches it
|
|
60
61
|
* router.post('/checkout', auth.protect(), async (req, res) => {
|
|
61
62
|
* const ok = await chargeCard(req.body.cardToken);
|
|
62
63
|
* if (!ok) throw new PaymentError('Card declined');
|
|
@@ -89,528 +90,44 @@ declare class SentriError extends Error {
|
|
|
89
90
|
* subclassing with a custom `code`.
|
|
90
91
|
*/
|
|
91
92
|
readonly statusCode: number;
|
|
92
|
-
/**
|
|
93
|
-
* @param code - Machine-readable error code. Use a built-in {@link SentriErrorCode}
|
|
94
|
-
* or any string for custom subclasses.
|
|
95
|
-
* @param message - Human-readable description of the error.
|
|
96
|
-
* @param statusCode - HTTP status to use in the response. For built-in codes
|
|
97
|
-
* this is derived automatically; for custom codes it defaults to `500`.
|
|
98
|
-
*/
|
|
99
93
|
constructor(code: SentriErrorCode | (string & {}), message: string, statusCode?: number);
|
|
100
94
|
}
|
|
101
95
|
|
|
102
|
-
/** Standard API response envelope returned by all built-in router endpoints. */
|
|
103
96
|
interface ApiResponse<T = null> {
|
|
104
97
|
error: boolean;
|
|
105
98
|
statusCode: number;
|
|
106
99
|
message: string;
|
|
107
100
|
data: T | null;
|
|
108
101
|
}
|
|
109
|
-
/**
|
|
110
|
-
interface
|
|
111
|
-
id: string;
|
|
112
|
-
/**
|
|
113
|
-
* The credential identifier for this user (email, username, phone number, etc.).
|
|
114
|
-
* The adapter decides which column(s) this maps to.
|
|
115
|
-
*/
|
|
116
|
-
identifier: string;
|
|
117
|
-
passwordHash: string;
|
|
118
|
-
/** Role names currently assigned to the user. */
|
|
119
|
-
roles: string[];
|
|
120
|
-
}
|
|
121
|
-
/** Shape of a session row returned by the adapter. */
|
|
122
|
-
interface SessionRecord {
|
|
102
|
+
/** A single identifier entry belonging to a user. */
|
|
103
|
+
interface IdentifierRecord {
|
|
123
104
|
id: string;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
/** Data the library passes to the adapter when creating a new user. */
|
|
129
|
-
interface CreateUserData {
|
|
130
|
-
/**
|
|
131
|
-
* The credential identifier supplied at registration (email, username, phone, etc.).
|
|
132
|
-
* Store this in whichever column(s) your schema uses for login lookup.
|
|
133
|
-
*/
|
|
134
|
-
identifier: string;
|
|
135
|
-
passwordHash: string;
|
|
136
|
-
/** Validated role names to assign at creation. */
|
|
137
|
-
roles: string[];
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* The database adapter interface the library depends on.
|
|
141
|
-
*
|
|
142
|
-
* Implement this to connect the library to any ORM or data layer.
|
|
143
|
-
*
|
|
144
|
-
* The library uses a single `identifier` string for credentials — your adapter
|
|
145
|
-
* decides what that means: email column, username column, phone column, or a
|
|
146
|
-
* query across multiple columns.
|
|
147
|
-
*/
|
|
148
|
-
interface AuthAdapter {
|
|
149
|
-
user: {
|
|
150
|
-
/**
|
|
151
|
-
* Find a user by their login identifier.
|
|
152
|
-
*
|
|
153
|
-
* The adapter decides which column(s) to query — email, username, phone,
|
|
154
|
-
* or a combined lookup (`WHERE email = $1 OR username = $1`).
|
|
155
|
-
* Returns `null` if not found.
|
|
156
|
-
*/
|
|
157
|
-
findByIdentifier(identifier: string): Promise<UserRecord | null>;
|
|
158
|
-
/** Find a user by their primary key. Returns `null` if not found. */
|
|
159
|
-
findById(id: string): Promise<UserRecord | null>;
|
|
160
|
-
/**
|
|
161
|
-
* Persist a new user with the given identifier, hashed password, and roles.
|
|
162
|
-
* The adapter maps `identifier` to the appropriate column(s) in your schema.
|
|
163
|
-
*/
|
|
164
|
-
create(data: CreateUserData): Promise<{
|
|
165
|
-
id: string;
|
|
166
|
-
}>;
|
|
167
|
-
/**
|
|
168
|
-
* Replace the complete role list for a user.
|
|
169
|
-
* Called by `assignRoles` after merging the new roles with the existing ones.
|
|
170
|
-
*/
|
|
171
|
-
updateRoles(userId: string, roles: string[]): Promise<void>;
|
|
172
|
-
};
|
|
173
|
-
session: {
|
|
174
|
-
/**
|
|
175
|
-
* Persist a new session and return its generated ID.
|
|
176
|
-
* `expiresAt` is computed from `refreshExpiresIn` in config.
|
|
177
|
-
*/
|
|
178
|
-
create(data: {
|
|
179
|
-
userId: string;
|
|
180
|
-
expiresAt: Date;
|
|
181
|
-
}): Promise<{
|
|
182
|
-
id: string;
|
|
183
|
-
}>;
|
|
184
|
-
/**
|
|
185
|
-
* Find a session by its ID, including the associated user.
|
|
186
|
-
* Returns `null` if the session does not exist (i.e. has been revoked).
|
|
187
|
-
*/
|
|
188
|
-
findById(sessionId: string): Promise<(SessionRecord & {
|
|
189
|
-
user: UserRecord;
|
|
190
|
-
}) | null>;
|
|
191
|
-
/** Delete a single session. Used during logout and token rotation. */
|
|
192
|
-
delete(sessionId: string): Promise<void>;
|
|
193
|
-
/** Delete all sessions belonging to a user. Used for "logout from all devices". */
|
|
194
|
-
deleteAllForUser(userId: string): Promise<void>;
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Custom service functions for the built-in auth router.
|
|
199
|
-
*
|
|
200
|
-
* Each key matches the internal service function name. When provided, the
|
|
201
|
-
* custom function replaces the default service call for that route while the
|
|
202
|
-
* router still handles request parsing, input validation, and response formatting.
|
|
203
|
-
*
|
|
204
|
-
* The function signatures mirror the internal services exactly but without the
|
|
205
|
-
* `config` parameter — the library passes config at bind time.
|
|
206
|
-
*
|
|
207
|
-
* @example
|
|
208
|
-
* createAuth({
|
|
209
|
-
* // ...
|
|
210
|
-
* router: {
|
|
211
|
-
* login: async (input) => {
|
|
212
|
-
* // add OTP check, custom user lookup, etc.
|
|
213
|
-
* // must return AuthResult
|
|
214
|
-
* },
|
|
215
|
-
* register: async (input) => {
|
|
216
|
-
* // send welcome email, set default profile, etc.
|
|
217
|
-
* // must return RegisterResult
|
|
218
|
-
* },
|
|
219
|
-
* },
|
|
220
|
-
* });
|
|
221
|
-
*/
|
|
222
|
-
interface RouterHandlers {
|
|
223
|
-
/**
|
|
224
|
-
* Replaces the default register service (`POST /register`).
|
|
225
|
-
*
|
|
226
|
-
* The router validates the request body (identifier, password, roles) first,
|
|
227
|
-
* then calls this function with the parsed input. Must return a `RegisterResult`.
|
|
228
|
-
* If omitted, the library's built-in registration logic runs instead.
|
|
229
|
-
*
|
|
230
|
-
* @example
|
|
231
|
-
* register: async (input) => {
|
|
232
|
-
* const result = await defaultRegister(input);
|
|
233
|
-
* if (result.success) {
|
|
234
|
-
* await emailService.sendWelcome(input.identifier);
|
|
235
|
-
* }
|
|
236
|
-
* return result;
|
|
237
|
-
* }
|
|
238
|
-
*/
|
|
239
|
-
register?: (input: RegisterInput) => Promise<RegisterResult>;
|
|
240
|
-
/**
|
|
241
|
-
* Replaces the default login service.
|
|
242
|
-
*
|
|
243
|
-
* The router validates the request body (identifier, password) first,
|
|
244
|
-
* then calls this function with the parsed input. Must return an `AuthResult`.
|
|
245
|
-
* If omitted, the library's built-in login logic runs instead.
|
|
246
|
-
*
|
|
247
|
-
* @example
|
|
248
|
-
* login: async (input) => {
|
|
249
|
-
* // verify OTP before issuing tokens
|
|
250
|
-
* const otpValid = await redis.get(`otp:${input.identifier}`);
|
|
251
|
-
* if (!otpValid) {
|
|
252
|
-
* return { success: false, error: new SentriError('INVALID_CREDENTIALS', 'OTP required') };
|
|
253
|
-
* }
|
|
254
|
-
* return defaultLogin(input);
|
|
255
|
-
* }
|
|
256
|
-
*/
|
|
257
|
-
login?: (input: LoginInput) => Promise<AuthResult>;
|
|
258
|
-
/**
|
|
259
|
-
* Replaces the default refresh service.
|
|
260
|
-
*
|
|
261
|
-
* Receives the raw refresh token string extracted from the cookie.
|
|
262
|
-
* Must return a `RefreshResult`. If omitted, the built-in session-rotation
|
|
263
|
-
* logic runs instead.
|
|
264
|
-
*
|
|
265
|
-
* @example
|
|
266
|
-
* refresh: async (refreshToken) => {
|
|
267
|
-
* const result = await defaultRefresh(refreshToken);
|
|
268
|
-
* if (result.success) {
|
|
269
|
-
* await auditLog.record('token_rotated', result.user.id);
|
|
270
|
-
* }
|
|
271
|
-
* return result;
|
|
272
|
-
* }
|
|
273
|
-
*/
|
|
274
|
-
refresh?: (refreshToken: string) => Promise<RefreshResult>;
|
|
275
|
-
/**
|
|
276
|
-
* Replaces the default logout service.
|
|
277
|
-
*
|
|
278
|
-
* Receives the raw refresh token from the cookie, or `undefined` if no cookie
|
|
279
|
-
* was present. The router clears the cookie after this function resolves.
|
|
280
|
-
* If omitted, the built-in session deletion logic runs instead.
|
|
281
|
-
*
|
|
282
|
-
* @example
|
|
283
|
-
* logout: async (refreshToken) => {
|
|
284
|
-
* if (refreshToken) {
|
|
285
|
-
* await defaultLogout(refreshToken);
|
|
286
|
-
* await auditLog.record('logout', refreshToken);
|
|
287
|
-
* }
|
|
288
|
-
* }
|
|
289
|
-
*/
|
|
290
|
-
logout?: (refreshToken: string | undefined) => Promise<void>;
|
|
291
|
-
/**
|
|
292
|
-
* Replaces the default logoutAll service.
|
|
293
|
-
*
|
|
294
|
-
* Receives the authenticated user's ID (from `req.user`, set by `protect()`).
|
|
295
|
-
* If omitted, the built-in "delete all sessions" logic runs instead.
|
|
296
|
-
*
|
|
297
|
-
* @example
|
|
298
|
-
* logoutAll: async (userId) => {
|
|
299
|
-
* await defaultLogoutAll(userId);
|
|
300
|
-
* await notifyService.push(userId, 'You have been signed out from all devices.');
|
|
301
|
-
* }
|
|
302
|
-
*/
|
|
303
|
-
logoutAll?: (userId: string) => Promise<void>;
|
|
304
|
-
/**
|
|
305
|
-
* Replaces the default assignRoles service.
|
|
306
|
-
*
|
|
307
|
-
* The router validates the request body and params first, then calls this
|
|
308
|
-
* function with the target `userId` and the validated `roles` array.
|
|
309
|
-
* Must return an `AssignRolesResult`. If omitted, the built-in role-merge
|
|
310
|
-
* logic runs instead.
|
|
311
|
-
*
|
|
312
|
-
* @example
|
|
313
|
-
* assignRoles: async (userId, roles) => {
|
|
314
|
-
* const result = await defaultAssignRoles(userId, roles);
|
|
315
|
-
* if (result.success) {
|
|
316
|
-
* await auditLog.record('roles_assigned', { userId, roles });
|
|
317
|
-
* }
|
|
318
|
-
* return result;
|
|
319
|
-
* }
|
|
320
|
-
*/
|
|
321
|
-
assignRoles?: (userId: string, roles: string[]) => Promise<AssignRolesResult>;
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Lifecycle hooks called by the built-in auth router at key points in the
|
|
325
|
-
* authentication flow. All hooks are optional and fire as side effects —
|
|
326
|
-
* returning a rejected Promise from a hook **does not** abort the request.
|
|
327
|
-
*
|
|
328
|
-
* Common uses: audit logging, metrics, rate-limit counters, notifications.
|
|
329
|
-
*
|
|
330
|
-
* @example
|
|
331
|
-
* createAuth({
|
|
332
|
-
* hooks: {
|
|
333
|
-
* onLogin: (user) => logger.info('login', { userId: user.id }),
|
|
334
|
-
* onFailedLogin: (identifier) => rateLimiter.hit(identifier),
|
|
335
|
-
* onLogout: (userId) => cache.invalidate(userId),
|
|
336
|
-
* },
|
|
337
|
-
* });
|
|
338
|
-
*/
|
|
339
|
-
interface AuthHooks {
|
|
340
|
-
/**
|
|
341
|
-
* Called after a successful login.
|
|
342
|
-
* Receives the authenticated user. Use for audit logs, login notifications, etc.
|
|
343
|
-
*/
|
|
344
|
-
onLogin?: (user: AuthUser) => void | Promise<void>;
|
|
345
|
-
/**
|
|
346
|
-
* Called after a failed login attempt (wrong password or unknown identifier).
|
|
347
|
-
* Receives the identifier that was attempted and the error.
|
|
348
|
-
* Use to increment rate-limit counters or alert on repeated failures.
|
|
349
|
-
*/
|
|
350
|
-
onFailedLogin?: (identifier: string, error: SentriError) => void | Promise<void>;
|
|
351
|
-
/**
|
|
352
|
-
* Called after a successful logout (single session or all sessions).
|
|
353
|
-
* Receives the user ID. Use for audit logs or cache invalidation.
|
|
354
|
-
*/
|
|
355
|
-
onLogout?: (userId: string) => void | Promise<void>;
|
|
356
|
-
}
|
|
357
|
-
/**
|
|
358
|
-
* Configuration passed to {@link createAuth}.
|
|
359
|
-
*
|
|
360
|
-
* Only `secret`, `validRoles`, and `adapter` are required.
|
|
361
|
-
* All other fields have sensible defaults.
|
|
362
|
-
*
|
|
363
|
-
* @example
|
|
364
|
-
* createAuth({
|
|
365
|
-
* secret: process.env.JWT_SECRET!,
|
|
366
|
-
* validRoles: ['user', 'admin'] as const,
|
|
367
|
-
* adapter: myAdapter,
|
|
368
|
-
* });
|
|
369
|
-
*/
|
|
370
|
-
interface AuthConfig<TRole extends string = string> {
|
|
371
|
-
/** Secret used to sign JWT tokens. Must not be empty. Keep this in an env variable. */
|
|
372
|
-
secret: string;
|
|
373
|
-
/**
|
|
374
|
-
* How long access tokens are valid.
|
|
375
|
-
* Accepts a duration string (`'15m'`, `'1h'`) or seconds as a number.
|
|
376
|
-
* @default '15m'
|
|
377
|
-
*/
|
|
378
|
-
accessExpiresIn?: string | number;
|
|
379
|
-
/**
|
|
380
|
-
* How long refresh tokens / sessions are valid.
|
|
381
|
-
* Accepts a duration string (`'7d'`, `'30d'`) or seconds as a number.
|
|
382
|
-
* @default '7d'
|
|
383
|
-
*/
|
|
384
|
-
refreshExpiresIn?: string | number;
|
|
385
|
-
/**
|
|
386
|
-
* HMAC signing algorithm used for JWTs.
|
|
387
|
-
* @default 'HS256'
|
|
388
|
-
*/
|
|
389
|
-
algorithm?: 'HS256' | 'HS384' | 'HS512';
|
|
390
|
-
/**
|
|
391
|
-
* bcrypt cost factor. Higher = slower hashing but more secure.
|
|
392
|
-
* @default 12
|
|
393
|
-
*/
|
|
394
|
-
saltRounds?: number;
|
|
395
|
-
/**
|
|
396
|
-
* Exhaustive list of role names your application uses.
|
|
397
|
-
* Registration will be rejected with `INVALID_ROLE` if a role outside this list is requested.
|
|
398
|
-
* Use `as const` to get TypeScript union-type safety on `authorize()`.
|
|
399
|
-
*
|
|
400
|
-
* @example
|
|
401
|
-
* validRoles: ['user', 'admin', 'moderator'] as const
|
|
402
|
-
*/
|
|
403
|
-
validRoles: readonly TRole[];
|
|
404
|
-
/** ORM adapter that connects the library to your database. */
|
|
405
|
-
adapter: AuthAdapter;
|
|
406
|
-
/**
|
|
407
|
-
* API key required to call `POST /register`.
|
|
408
|
-
*
|
|
409
|
-
* When set, the `/register` endpoint expects an `X-Api-Key` header whose
|
|
410
|
-
* value matches this string exactly. Requests without the header, or with
|
|
411
|
-
* the wrong value, are rejected with HTTP 401 (`UNAUTHORIZED`).
|
|
412
|
-
*
|
|
413
|
-
* Use this to restrict self-registration — for example, only your own
|
|
414
|
-
* back-office service or admin panel should be able to create new accounts,
|
|
415
|
-
* so you never expose user registration to arbitrary callers.
|
|
416
|
-
*
|
|
417
|
-
* @example
|
|
418
|
-
* createAuth({
|
|
419
|
-
* // ...
|
|
420
|
-
* apiKey: process.env.REGISTER_API_KEY!,
|
|
421
|
-
* });
|
|
422
|
-
*
|
|
423
|
-
* // Client must send:
|
|
424
|
-
* // POST /auth/register
|
|
425
|
-
* // X-Api-Key: <value of REGISTER_API_KEY>
|
|
426
|
-
*/
|
|
427
|
-
apiKey?: string;
|
|
428
|
-
/**
|
|
429
|
-
* Custom service functions for individual routes in the built-in auth router.
|
|
430
|
-
*
|
|
431
|
-
* The router still handles request parsing, validation, and response formatting.
|
|
432
|
-
* Only the core service logic is replaced by your function.
|
|
433
|
-
*
|
|
434
|
-
* @example
|
|
435
|
-
* createAuth({
|
|
436
|
-
* // ...
|
|
437
|
-
* router: {
|
|
438
|
-
* login: async (input) => {
|
|
439
|
-
* // verify OTP, then delegate to default or return custom result
|
|
440
|
-
* return { success: true, accessToken, refreshToken, user };
|
|
441
|
-
* },
|
|
442
|
-
* register: async (input) => {
|
|
443
|
-
* // send welcome email after successful registration
|
|
444
|
-
* const result = await defaultRegister(input);
|
|
445
|
-
* if (result.success) await emailService.sendWelcome(input.identifier);
|
|
446
|
-
* return result;
|
|
447
|
-
* },
|
|
448
|
-
* },
|
|
449
|
-
* });
|
|
450
|
-
*/
|
|
451
|
-
router?: RouterHandlers;
|
|
452
|
-
/**
|
|
453
|
-
* When set, the built-in router (`auth.router()`) stores the refresh token
|
|
454
|
-
* in an httpOnly cookie instead of returning it in the response body.
|
|
455
|
-
*
|
|
456
|
-
* The `refreshToken` field is omitted from `/login` and `/refresh` responses.
|
|
457
|
-
* The `/logout` and `/logout-all` routes automatically clear the cookie.
|
|
458
|
-
*
|
|
459
|
-
* No extra middleware (e.g. `cookie-parser`) is required.
|
|
460
|
-
*
|
|
461
|
-
* @example
|
|
462
|
-
* createAuth({
|
|
463
|
-
* // ...
|
|
464
|
-
* cookie: { secure: process.env.NODE_ENV === 'production' },
|
|
465
|
-
* });
|
|
466
|
-
*/
|
|
467
|
-
cookie?: CookieConfig;
|
|
468
|
-
/**
|
|
469
|
-
* Lifecycle hooks called at key points in the auth flow.
|
|
470
|
-
*
|
|
471
|
-
* All hooks fire as side effects — a rejected hook Promise is silently
|
|
472
|
-
* swallowed so a broken hook can never take down a login request.
|
|
473
|
-
*
|
|
474
|
-
* @example
|
|
475
|
-
* createAuth({
|
|
476
|
-
* hooks: {
|
|
477
|
-
* onLogin: async (user) => {
|
|
478
|
-
* await auditLog.record('login', user.id);
|
|
479
|
-
* },
|
|
480
|
-
* onFailedLogin: (identifier) => {
|
|
481
|
-
* rateLimiter.hit(`login:${identifier}`);
|
|
482
|
-
* },
|
|
483
|
-
* },
|
|
484
|
-
* });
|
|
485
|
-
*/
|
|
486
|
-
hooks?: AuthHooks;
|
|
487
|
-
/**
|
|
488
|
-
* Optional guard called by `protect()` after verifying the access token's
|
|
489
|
-
* signature and expiry. Return `true` to reject the token immediately, even
|
|
490
|
-
* if it is cryptographically valid.
|
|
491
|
-
*
|
|
492
|
-
* Use this for **immediate token revocation** — for example, store revoked
|
|
493
|
-
* `sessionId` values in Redis and check membership here:
|
|
494
|
-
*
|
|
495
|
-
* ```typescript
|
|
496
|
-
* isTokenRevoked: async (sessionId) =>
|
|
497
|
-
* await redis.sismember('revoked_sessions', sessionId),
|
|
498
|
-
* ```
|
|
499
|
-
*
|
|
500
|
-
* **Performance note:** this function is called on every protected request.
|
|
501
|
-
* Keep it fast (e.g. a single Redis GET/SISMEMBER) or the latency benefit
|
|
502
|
-
* of stateless access tokens is lost. If you can tolerate a short revocation
|
|
503
|
-
* window equal to `accessExpiresIn`, omit this hook entirely.
|
|
504
|
-
*
|
|
505
|
-
* @param sessionId - The `sessionId` embedded in the access token at login time.
|
|
506
|
-
* @returns `true` if the token should be rejected, `false` (or `undefined`) to allow.
|
|
507
|
-
*/
|
|
508
|
-
isTokenRevoked?: (sessionId: string) => boolean | Promise<boolean>;
|
|
509
|
-
/**
|
|
510
|
-
* When set, the built-in router also stores the **access token** in a
|
|
511
|
-
* non-httpOnly cookie so browser JavaScript can read it via
|
|
512
|
-
* `document.cookie` or the `getCurrentAccessToken` helper.
|
|
513
|
-
*
|
|
514
|
-
* `protect()` reads the access token from this cookie when no
|
|
515
|
-
* `Authorization: Bearer` header is present, so clients can skip the
|
|
516
|
-
* manual header management entirely.
|
|
517
|
-
*
|
|
518
|
-
* The cookie's `max-age` is derived automatically from `config.accessExpiresIn`.
|
|
519
|
-
* When `protect()` performs a silent refresh, the access token cookie is
|
|
520
|
-
* updated in the same response.
|
|
521
|
-
*
|
|
522
|
-
* @example
|
|
523
|
-
* createAuth({
|
|
524
|
-
* accessExpiresIn: '5m',
|
|
525
|
-
* accessCookie: { secure: process.env.NODE_ENV === 'production' },
|
|
526
|
-
* cookie: { secure: process.env.NODE_ENV === 'production' },
|
|
527
|
-
* });
|
|
528
|
-
*/
|
|
529
|
-
accessCookie?: AccessCookieConfig;
|
|
530
|
-
}
|
|
531
|
-
/**
|
|
532
|
-
* Cookie settings for storing the refresh token in an httpOnly cookie.
|
|
533
|
-
* All fields are optional — defaults are chosen for security.
|
|
534
|
-
*/
|
|
535
|
-
interface CookieConfig {
|
|
536
|
-
/**
|
|
537
|
-
* Name of the cookie.
|
|
538
|
-
* @default 'refresh_token'
|
|
539
|
-
*/
|
|
540
|
-
name?: string;
|
|
541
|
-
/**
|
|
542
|
-
* Prevents JavaScript from reading the cookie (`document.cookie`).
|
|
543
|
-
* @default true
|
|
544
|
-
*/
|
|
545
|
-
httpOnly?: boolean;
|
|
546
|
-
/**
|
|
547
|
-
* Restricts the cookie to HTTPS connections.
|
|
548
|
-
* Set to `true` in production.
|
|
549
|
-
* @default false
|
|
550
|
-
*/
|
|
551
|
-
secure?: boolean;
|
|
552
|
-
/**
|
|
553
|
-
* Controls cross-site request behaviour.
|
|
554
|
-
* @default 'strict'
|
|
555
|
-
*/
|
|
556
|
-
sameSite?: 'strict' | 'lax' | 'none';
|
|
557
|
-
/**
|
|
558
|
-
* URL path the cookie is scoped to.
|
|
559
|
-
* @default '/'
|
|
560
|
-
*/
|
|
561
|
-
path?: string;
|
|
562
|
-
}
|
|
563
|
-
/**
|
|
564
|
-
* Cookie settings for storing the access token in a **non**-httpOnly cookie.
|
|
565
|
-
*
|
|
566
|
-
* Because `httpOnly` is intentionally `false`, browser JavaScript can read this
|
|
567
|
-
* cookie via `document.cookie` or the `getCurrentAccessToken` helper, which makes
|
|
568
|
-
* it convenient for SPAs that need to attach the token to outgoing requests.
|
|
569
|
-
*
|
|
570
|
-
* The cookie's `max-age` is derived automatically from `config.accessExpiresIn`.
|
|
571
|
-
*
|
|
572
|
-
* All fields are optional — safe defaults are applied.
|
|
573
|
-
*/
|
|
574
|
-
interface AccessCookieConfig {
|
|
575
|
-
/**
|
|
576
|
-
* Name of the access token cookie.
|
|
577
|
-
* @default 'access_token'
|
|
578
|
-
*/
|
|
579
|
-
name?: string;
|
|
580
|
-
/**
|
|
581
|
-
* Restricts the cookie to HTTPS connections.
|
|
582
|
-
* Set to `true` in production.
|
|
583
|
-
* @default false
|
|
584
|
-
*/
|
|
585
|
-
secure?: boolean;
|
|
586
|
-
/**
|
|
587
|
-
* Controls cross-site request behaviour.
|
|
588
|
-
* @default 'strict'
|
|
589
|
-
*/
|
|
590
|
-
sameSite?: 'strict' | 'lax' | 'none';
|
|
591
|
-
/**
|
|
592
|
-
* URL path the cookie is scoped to.
|
|
593
|
-
* @default '/'
|
|
594
|
-
*/
|
|
595
|
-
path?: string;
|
|
105
|
+
type: string;
|
|
106
|
+
value: string;
|
|
107
|
+
isPrimary: boolean;
|
|
596
108
|
}
|
|
597
109
|
/**
|
|
598
|
-
* The user
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
* validated against the database on every request. Tokens from older
|
|
602
|
-
* versions that lack this claim are accepted but bypass session validation.
|
|
110
|
+
* The authenticated user shape. `identifier` and `identifierType` always
|
|
111
|
+
* reflect the primary identifier. `identifiers` is populated only when the
|
|
112
|
+
* full user object is fetched (e.g. GET /me) and is absent from JWT payloads.
|
|
603
113
|
*/
|
|
604
114
|
interface AuthUser<TRole extends string = string> {
|
|
605
115
|
id: string;
|
|
606
|
-
/**
|
|
607
|
-
* The credential identifier for this user (email, username, phone, etc.).
|
|
608
|
-
* Reflects whatever value was passed as `identifier` at registration or login.
|
|
609
|
-
*/
|
|
116
|
+
/** Primary identifier value — embedded in the JWT payload. */
|
|
610
117
|
identifier: string;
|
|
118
|
+
/** Type of the primary identifier (e.g. 'email', 'username'). */
|
|
119
|
+
identifierType: string;
|
|
611
120
|
roles: TRole[];
|
|
121
|
+
/** All identifiers for this user. Only present in full user responses, not in JWT. */
|
|
122
|
+
identifiers?: IdentifierRecord[];
|
|
123
|
+
}
|
|
124
|
+
/** Input for a single identifier entry. */
|
|
125
|
+
interface IdentifierInput {
|
|
126
|
+
/** Arbitrary label such as 'email', 'username', or 'phone'. */
|
|
127
|
+
type: string;
|
|
128
|
+
/** The globally unique identifier value. */
|
|
129
|
+
value: string;
|
|
612
130
|
}
|
|
613
|
-
/** Return type of `register`. */
|
|
614
131
|
type RegisterResult<TRole extends string = string> = {
|
|
615
132
|
success: true;
|
|
616
133
|
user: AuthUser<TRole>;
|
|
@@ -618,7 +135,6 @@ type RegisterResult<TRole extends string = string> = {
|
|
|
618
135
|
success: false;
|
|
619
136
|
error: SentriError;
|
|
620
137
|
};
|
|
621
|
-
/** Return type of `login`. */
|
|
622
138
|
type AuthResult<TRole extends string = string> = {
|
|
623
139
|
success: true;
|
|
624
140
|
accessToken: string;
|
|
@@ -628,7 +144,6 @@ type AuthResult<TRole extends string = string> = {
|
|
|
628
144
|
success: false;
|
|
629
145
|
error: SentriError;
|
|
630
146
|
};
|
|
631
|
-
/** Return type of `assignRoles`. */
|
|
632
147
|
type AssignRolesResult<TRole extends string = string> = {
|
|
633
148
|
success: true;
|
|
634
149
|
user: AuthUser<TRole>;
|
|
@@ -636,7 +151,33 @@ type AssignRolesResult<TRole extends string = string> = {
|
|
|
636
151
|
success: false;
|
|
637
152
|
error: SentriError;
|
|
638
153
|
};
|
|
639
|
-
|
|
154
|
+
type GetUserResult<TRole extends string = string> = {
|
|
155
|
+
success: true;
|
|
156
|
+
user: AuthUser<TRole>;
|
|
157
|
+
} | {
|
|
158
|
+
success: false;
|
|
159
|
+
error: SentriError;
|
|
160
|
+
};
|
|
161
|
+
type BulkIdentifiersResult = {
|
|
162
|
+
success: true;
|
|
163
|
+
identifiers: IdentifierRecord[];
|
|
164
|
+
} | {
|
|
165
|
+
success: false;
|
|
166
|
+
error: SentriError;
|
|
167
|
+
};
|
|
168
|
+
type ChangePrimaryResult = {
|
|
169
|
+
success: true;
|
|
170
|
+
identifiers: IdentifierRecord[];
|
|
171
|
+
} | {
|
|
172
|
+
success: false;
|
|
173
|
+
error: SentriError;
|
|
174
|
+
};
|
|
175
|
+
type ChangePasswordResult = {
|
|
176
|
+
success: true;
|
|
177
|
+
} | {
|
|
178
|
+
success: false;
|
|
179
|
+
error: SentriError;
|
|
180
|
+
};
|
|
640
181
|
type RefreshResult<TRole extends string = string> = {
|
|
641
182
|
success: true;
|
|
642
183
|
accessToken: string;
|
|
@@ -646,26 +187,100 @@ type RefreshResult<TRole extends string = string> = {
|
|
|
646
187
|
success: false;
|
|
647
188
|
error: SentriError;
|
|
648
189
|
};
|
|
649
|
-
/** Input for `register`. */
|
|
650
190
|
interface RegisterInput<TRole extends string = string> {
|
|
651
191
|
/**
|
|
652
|
-
*
|
|
653
|
-
*
|
|
192
|
+
* One or more identifiers for the new user. The first entry becomes the
|
|
193
|
+
* primary identifier (embedded in the JWT payload). All values must be
|
|
194
|
+
* globally unique. At least one identifier is required.
|
|
654
195
|
*/
|
|
655
|
-
|
|
196
|
+
identifiers: IdentifierInput[];
|
|
656
197
|
password: string;
|
|
657
|
-
/** Roles to assign at creation. Must be a subset of `validRoles`. */
|
|
658
198
|
roles?: TRole[];
|
|
659
199
|
}
|
|
660
|
-
/** Input for `login`. */
|
|
661
200
|
interface LoginInput {
|
|
662
|
-
/**
|
|
663
|
-
* The user's login credential — email, username, phone number, or any unique string.
|
|
664
|
-
* The adapter's `findByIdentifier` handles the lookup.
|
|
665
|
-
*/
|
|
201
|
+
/** Any of the user's identifier values — Sentri searches all types. */
|
|
666
202
|
identifier: string;
|
|
667
203
|
password: string;
|
|
668
204
|
}
|
|
205
|
+
interface CookieConfig {
|
|
206
|
+
name?: string;
|
|
207
|
+
httpOnly?: boolean;
|
|
208
|
+
secure?: boolean;
|
|
209
|
+
sameSite?: 'strict' | 'lax' | 'none';
|
|
210
|
+
path?: string;
|
|
211
|
+
}
|
|
212
|
+
interface AccessCookieConfig {
|
|
213
|
+
name?: string;
|
|
214
|
+
secure?: boolean;
|
|
215
|
+
sameSite?: 'strict' | 'lax' | 'none';
|
|
216
|
+
path?: string;
|
|
217
|
+
}
|
|
218
|
+
interface AuthHooks {
|
|
219
|
+
onLogin?: (user: AuthUser) => void | Promise<void>;
|
|
220
|
+
onFailedLogin?: (identifier: string, error: SentriError) => void | Promise<void>;
|
|
221
|
+
onLogout?: (userId: string) => void | Promise<void>;
|
|
222
|
+
}
|
|
223
|
+
interface RouterHandlers {
|
|
224
|
+
register?: (input: RegisterInput) => Promise<RegisterResult>;
|
|
225
|
+
login?: (input: LoginInput) => Promise<AuthResult>;
|
|
226
|
+
refresh?: (refreshToken: string) => Promise<RefreshResult>;
|
|
227
|
+
logout?: (refreshToken: string | undefined) => Promise<void>;
|
|
228
|
+
logoutAll?: (userId: string) => Promise<void>;
|
|
229
|
+
assignRoles?: (userId: string, roles: string[]) => Promise<AssignRolesResult>;
|
|
230
|
+
bulkCreateIdentifiers?: (userId: string, identifiers: IdentifierInput[]) => Promise<BulkIdentifiersResult>;
|
|
231
|
+
bulkUpdateIdentifiers?: (userId: string, updates: Array<{
|
|
232
|
+
id: string;
|
|
233
|
+
type: string;
|
|
234
|
+
value: string;
|
|
235
|
+
}>) => Promise<BulkIdentifiersResult>;
|
|
236
|
+
bulkDeleteIdentifiers?: (userId: string, ids: string[]) => Promise<BulkIdentifiersResult>;
|
|
237
|
+
changePrimaryIdentifier?: (userId: string, identifierId: string) => Promise<ChangePrimaryResult>;
|
|
238
|
+
changePassword?: (userId: string, currentPassword: string, newPassword: string) => Promise<ChangePasswordResult>;
|
|
239
|
+
}
|
|
240
|
+
interface ServerAuthConfig<TRole extends string = string> {
|
|
241
|
+
mode: 'server';
|
|
242
|
+
/** Kysely Dialect (e.g. PostgresDialect, MysqlDialect, SqliteDialect). */
|
|
243
|
+
dialect: Dialect;
|
|
244
|
+
/**
|
|
245
|
+
* JWT signing secret.
|
|
246
|
+
* - HS256/HS384/HS512: plain string, minimum 32 characters.
|
|
247
|
+
* - RS256/RS384/RS512: RSA private key in PEM format.
|
|
248
|
+
*/
|
|
249
|
+
secret: string;
|
|
250
|
+
/**
|
|
251
|
+
* JWT signing algorithm.
|
|
252
|
+
* Use RS256/RS384/RS512 to enable the GET /keys endpoint for SSO.
|
|
253
|
+
* @default 'HS256'
|
|
254
|
+
*/
|
|
255
|
+
algorithm?: 'HS256' | 'HS384' | 'HS512' | 'RS256' | 'RS384' | 'RS512';
|
|
256
|
+
validRoles: readonly TRole[];
|
|
257
|
+
/** @default '15m' */
|
|
258
|
+
accessExpiresIn?: string | number;
|
|
259
|
+
/** @default '7d' */
|
|
260
|
+
refreshExpiresIn?: string | number;
|
|
261
|
+
/** @default 12 */
|
|
262
|
+
saltRounds?: number;
|
|
263
|
+
apiKey?: string;
|
|
264
|
+
cookie?: CookieConfig;
|
|
265
|
+
accessCookie?: AccessCookieConfig;
|
|
266
|
+
hooks?: AuthHooks;
|
|
267
|
+
router?: RouterHandlers;
|
|
268
|
+
isTokenRevoked?: (sessionId: string) => boolean | Promise<boolean>;
|
|
269
|
+
/**
|
|
270
|
+
* Redis connection URL (e.g. `redis://localhost:6379`).
|
|
271
|
+
* When set, `auth.idempotencyMiddleware()` uses Redis as the cache backend
|
|
272
|
+
* instead of an in-memory Map — required for multi-process deployments.
|
|
273
|
+
*/
|
|
274
|
+
redisUrl?: string;
|
|
275
|
+
}
|
|
276
|
+
interface ClientAuthConfig<TRole extends string = string> {
|
|
277
|
+
mode: 'client';
|
|
278
|
+
/** URL of the auth server's public key endpoint (e.g. https://auth.myapp.com/auth/keys). */
|
|
279
|
+
keyUri: string;
|
|
280
|
+
/** Optional — only needed for TypeScript type safety on authorize(). */
|
|
281
|
+
validRoles?: readonly TRole[];
|
|
282
|
+
}
|
|
283
|
+
type AuthConfig<TRole extends string = string> = ServerAuthConfig<TRole> | ClientAuthConfig<TRole>;
|
|
669
284
|
|
|
670
285
|
/** A function that determines whether the current request is permitted. */
|
|
671
286
|
type PermitCheck = (request: Request) => boolean | Promise<boolean>;
|
|
@@ -766,288 +381,226 @@ interface ErrorHandlerOptions {
|
|
|
766
381
|
*/
|
|
767
382
|
declare function createErrorHandler(options?: ErrorHandlerOptions): ErrorRequestHandler;
|
|
768
383
|
|
|
384
|
+
interface IdempotencyOptions {
|
|
385
|
+
/** @default 300_000 (5 minutes) */
|
|
386
|
+
ttl?: number;
|
|
387
|
+
/** @default 'X-Idempotency-Key' */
|
|
388
|
+
header?: string;
|
|
389
|
+
/** @default ['POST', 'PUT', 'PATCH'] */
|
|
390
|
+
methods?: string[];
|
|
391
|
+
/**
|
|
392
|
+
* Max in-memory entries (ignored when redisUrl is set).
|
|
393
|
+
* @default 10_000
|
|
394
|
+
*/
|
|
395
|
+
maxSize?: number;
|
|
396
|
+
/**
|
|
397
|
+
* Redis connection URL (e.g. `redis://localhost:6379`).
|
|
398
|
+
* When set, uses Redis as the cache backend instead of in-memory Map.
|
|
399
|
+
*/
|
|
400
|
+
redisUrl?: string;
|
|
401
|
+
}
|
|
769
402
|
/**
|
|
770
|
-
*
|
|
403
|
+
* Middleware that deduplicates non-idempotent requests.
|
|
404
|
+
*
|
|
405
|
+
* When a request arrives with a matching idempotency key header, the cached
|
|
406
|
+
* response is replayed immediately — the handler is not called again.
|
|
407
|
+
* Responses are only cached for 2xx status codes.
|
|
771
408
|
*
|
|
772
|
-
*
|
|
773
|
-
*
|
|
409
|
+
* Two backends are available:
|
|
410
|
+
* - **In-memory** (default) — zero dependencies, single-process only.
|
|
411
|
+
* - **Redis** — set `redisUrl` to share state across processes/instances.
|
|
412
|
+
*
|
|
413
|
+
* When using `createAuthServer()`, prefer `auth.idempotencyMiddleware()` instead —
|
|
414
|
+
* it automatically inherits the `redisUrl` from server config.
|
|
415
|
+
*
|
|
416
|
+
* @example
|
|
417
|
+
* // Standalone usage
|
|
418
|
+
* app.use(createIdempotencyMiddleware({ ttl: 60_000 }));
|
|
774
419
|
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
420
|
+
* @example
|
|
421
|
+
* // Multi-process (Redis)
|
|
422
|
+
* app.use(createIdempotencyMiddleware({ redisUrl: 'redis://localhost:6379' }));
|
|
777
423
|
*/
|
|
424
|
+
declare function createIdempotencyMiddleware(options?: IdempotencyOptions): RequestHandler;
|
|
425
|
+
|
|
778
426
|
interface AuthClient<TRole extends string = string> {
|
|
779
|
-
/**
|
|
780
|
-
* Express middleware factory that enforces authentication.
|
|
781
|
-
*
|
|
782
|
-
* Reads the `Authorization: Bearer <token>` header, verifies the access token,
|
|
783
|
-
* confirms the session is still active in the database, and injects the decoded
|
|
784
|
-
* payload as `request.user`. Calls `next(SentriError)` on any failure.
|
|
785
|
-
*
|
|
786
|
-
* @example
|
|
787
|
-
* router.get('/me', auth.protect(), (request, response) => {
|
|
788
|
-
* response.json(request.user);
|
|
789
|
-
* });
|
|
790
|
-
*/
|
|
427
|
+
/** JWT authentication middleware. Reads Bearer token or access_token cookie. */
|
|
791
428
|
protect(): RequestHandler;
|
|
792
|
-
/**
|
|
793
|
-
* Express middleware factory that enforces role-based access.
|
|
794
|
-
*
|
|
795
|
-
* Must be used **after** `protect()`. Passes if the authenticated user has
|
|
796
|
-
* at least one of the specified roles; otherwise calls `next(SentriError)` with
|
|
797
|
-
* code `FORBIDDEN`.
|
|
798
|
-
*
|
|
799
|
-
* @example
|
|
800
|
-
* router.delete('/posts/:id', auth.protect(), auth.authorize('admin'), handler);
|
|
801
|
-
*/
|
|
429
|
+
/** Role-based access middleware. Must follow protect(). */
|
|
802
430
|
authorize(...roles: TRole[]): RequestHandler;
|
|
803
|
-
/**
|
|
804
|
-
* Express middleware factory for resource-level permission checks.
|
|
805
|
-
*
|
|
806
|
-
* Must be used **after** `protect()`. Evaluates a check function against the
|
|
807
|
-
* current request and calls `next(SentriError)` with `FORBIDDEN` if it returns `false`.
|
|
808
|
-
*
|
|
809
|
-
* Accepts either a bare check function or an options object with an optional
|
|
810
|
-
* `roles` list whose members bypass the check entirely.
|
|
811
|
-
*
|
|
812
|
-
* @example
|
|
813
|
-
* // User can only update their own profile
|
|
814
|
-
* router.put('/users/:id',
|
|
815
|
-
* auth.protect(),
|
|
816
|
-
* auth.permit((request) => request.user!.id === request.params['id']),
|
|
817
|
-
* handler,
|
|
818
|
-
* );
|
|
819
|
-
*
|
|
820
|
-
* @example
|
|
821
|
-
* // Admins bypass the check; others must own the resource
|
|
822
|
-
* router.delete('/posts/:id',
|
|
823
|
-
* auth.protect(),
|
|
824
|
-
* auth.permit({
|
|
825
|
-
* roles: ['admin'],
|
|
826
|
-
* check: async (request) => {
|
|
827
|
-
* const post = await db.post.findUnique({ where: { id: request.params['id'] } });
|
|
828
|
-
* return post?.authorId === request.user!.id;
|
|
829
|
-
* },
|
|
830
|
-
* }),
|
|
831
|
-
* handler,
|
|
832
|
-
* );
|
|
833
|
-
*/
|
|
431
|
+
/** Resource-level permission middleware. Must follow protect(). */
|
|
834
432
|
permit(check: PermitCheck): RequestHandler;
|
|
835
433
|
permit(options: PermitOptions<TRole>): RequestHandler;
|
|
836
|
-
/**
|
|
434
|
+
/** Global error handler middleware. Mount after all routes. */
|
|
435
|
+
errorHandler(options?: ErrorHandlerOptions): ErrorRequestHandler;
|
|
436
|
+
}
|
|
437
|
+
interface ServerAuthClient<TRole extends string = string> extends AuthClient<TRole> {
|
|
438
|
+
/** Hash a plain-text password. */
|
|
837
439
|
hashPassword(plain: string): Promise<string>;
|
|
838
|
-
/** Compare a plain-text password against a
|
|
440
|
+
/** Compare a plain-text password against a bcrypt hash. */
|
|
839
441
|
verifyPassword(plain: string, hash: string): Promise<boolean>;
|
|
840
|
-
/** Sign an access token
|
|
442
|
+
/** Sign an access token. */
|
|
841
443
|
signAccessToken(payload: AuthUser<TRole>): string;
|
|
842
|
-
/** Sign a refresh token
|
|
444
|
+
/** Sign a refresh token. */
|
|
843
445
|
signRefreshToken(sessionId: string): string;
|
|
844
|
-
/** Verify
|
|
446
|
+
/** Verify an access token. */
|
|
845
447
|
verifyAccessToken(token: string): AuthUser<TRole>;
|
|
846
|
-
/** Verify
|
|
448
|
+
/** Verify a refresh token. */
|
|
847
449
|
verifyRefreshToken(token: string): {
|
|
848
450
|
sessionId: string;
|
|
849
451
|
};
|
|
850
|
-
/**
|
|
851
|
-
* Extract the raw access token string from an incoming Express request.
|
|
852
|
-
*
|
|
853
|
-
* Checks two sources in order:
|
|
854
|
-
* 1. `Authorization: Bearer <token>` header
|
|
855
|
-
* 2. Access token cookie (name from `config.accessCookie.name`, default `'access_token'`)
|
|
856
|
-
*
|
|
857
|
-
* Returns `undefined` when neither source is present.
|
|
858
|
-
*
|
|
859
|
-
* Useful in custom middleware or logging where you need the raw token without
|
|
860
|
-
* running full JWT verification.
|
|
861
|
-
*
|
|
862
|
-
* @example
|
|
863
|
-
* app.use((req, _res, next) => {
|
|
864
|
-
* const token = auth.getCurrentAccessToken(req);
|
|
865
|
-
* if (token) logger.debug('access token present');
|
|
866
|
-
* next();
|
|
867
|
-
* });
|
|
868
|
-
*/
|
|
452
|
+
/** Extract the raw access token from an Express request. */
|
|
869
453
|
getCurrentAccessToken(request: Request): string | undefined;
|
|
870
454
|
/**
|
|
871
|
-
*
|
|
872
|
-
*
|
|
873
|
-
* Endpoints:
|
|
874
|
-
* - `POST /register` — register a new user. Requires `X-Api-Key` header when `config.apiKey` is set.
|
|
875
|
-
* - `POST /login` — authenticate, sets refresh token cookie, returns `{ accessToken, user }`
|
|
876
|
-
* - `POST /refresh` — rotate refresh token, returns new `{ accessToken }`
|
|
877
|
-
* - `POST /logout` — delete the current session; the bound access token is immediately rejected by `protect()`
|
|
878
|
-
* - `POST /logout-all` — delete all sessions for the user (requires valid access token)
|
|
879
|
-
* - `GET /me` — return the authenticated user
|
|
880
|
-
* - `POST /users/:userId/roles` — assign roles (requires admin)
|
|
881
|
-
*
|
|
882
|
-
* Requires `express.json()` before the router.
|
|
883
|
-
*
|
|
884
|
-
* @example
|
|
885
|
-
* app.use(express.json());
|
|
886
|
-
* app.use('/auth', auth.router());
|
|
455
|
+
* Pre-built Express Router with auth endpoints.
|
|
456
|
+
* See README for the full endpoint list.
|
|
887
457
|
*/
|
|
888
458
|
router(): Router;
|
|
889
459
|
/**
|
|
890
|
-
*
|
|
891
|
-
*
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
*
|
|
896
|
-
*
|
|
897
|
-
*
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
460
|
+
* Run database migrations to create sentri_users, sentri_sessions, and
|
|
461
|
+
* sentri_identifiers tables. Safe to call on every startup — uses IF NOT EXISTS.
|
|
462
|
+
*/
|
|
463
|
+
migrate(): Promise<void>;
|
|
464
|
+
/**
|
|
465
|
+
* Idempotency middleware. Caches successful responses and replays them for
|
|
466
|
+
* duplicate requests with the same idempotency key header.
|
|
467
|
+
* Uses Redis backend when `redisUrl` is set in server config; otherwise in-memory.
|
|
468
|
+
*/
|
|
469
|
+
idempotencyMiddleware(options?: IdempotencyOptions): RequestHandler;
|
|
470
|
+
/** Register a new user with one or more identifiers. */
|
|
471
|
+
register(input: RegisterInput<TRole>): Promise<RegisterResult<TRole>>;
|
|
472
|
+
/** Authenticate a user by any of their identifier values, returns access + refresh tokens. */
|
|
473
|
+
login(input: LoginInput): Promise<AuthResult<TRole>>;
|
|
474
|
+
/** Rotate a refresh token, returns new token pair. */
|
|
475
|
+
refresh(refreshToken: string): Promise<RefreshResult<TRole>>;
|
|
476
|
+
/** Invalidate the session associated with the given refresh token. */
|
|
477
|
+
logout(refreshToken: string): Promise<void>;
|
|
478
|
+
/** Invalidate all sessions for a user. */
|
|
479
|
+
logoutAll(userId: string): Promise<void>;
|
|
480
|
+
/** Fetch a user by ID, including all their identifiers. Returns `success: false` if not found. */
|
|
481
|
+
getUser(userId: string): Promise<GetUserResult<TRole>>;
|
|
482
|
+
/** Change a user's password and revoke all their sessions. */
|
|
483
|
+
changePassword(userId: string, currentPassword: string, newPassword: string): Promise<ChangePasswordResult>;
|
|
484
|
+
/** Add roles to a user (existing roles are preserved). */
|
|
485
|
+
assignRoles(userId: string, roles: TRole[]): Promise<AssignRolesResult<TRole>>;
|
|
486
|
+
/** Add new identifiers to a user in bulk. Returns the full updated identifier list. */
|
|
487
|
+
bulkCreateIdentifiers(userId: string, identifiers: IdentifierInput[]): Promise<BulkIdentifiersResult>;
|
|
488
|
+
/** Update type and value for multiple identifiers in bulk. Returns the full updated identifier list. */
|
|
489
|
+
bulkUpdateIdentifiers(userId: string, updates: Array<{
|
|
490
|
+
id: string;
|
|
491
|
+
type: string;
|
|
492
|
+
value: string;
|
|
493
|
+
}>): Promise<BulkIdentifiersResult>;
|
|
494
|
+
/** Delete multiple identifiers by ID. At least one identifier must remain. Returns the remaining list. */
|
|
495
|
+
bulkDeleteIdentifiers(userId: string, ids: string[]): Promise<BulkIdentifiersResult>;
|
|
496
|
+
/** Change which identifier is marked as primary (embedded in JWT on next login/refresh). */
|
|
497
|
+
changePrimaryIdentifier(userId: string, identifierId: string): Promise<ChangePrimaryResult>;
|
|
498
|
+
}
|
|
499
|
+
interface ClientAuthClient<TRole extends string = string> extends AuthClient<TRole> {
|
|
923
500
|
}
|
|
924
501
|
/**
|
|
925
|
-
* Create a
|
|
502
|
+
* Create a Sentri auth client.
|
|
926
503
|
*
|
|
927
|
-
* Pass
|
|
928
|
-
*
|
|
929
|
-
*
|
|
504
|
+
* Pass `mode: 'server'` to get a full auth server with database, JWT signing,
|
|
505
|
+
* and built-in endpoints. Pass `mode: 'client'` to get a stateless verifier
|
|
506
|
+
* that validates tokens via the server's JWKS endpoint.
|
|
930
507
|
*
|
|
931
|
-
*
|
|
932
|
-
*
|
|
508
|
+
* For server mode with PostgreSQL, prefer `createAuthServer()` — it handles
|
|
509
|
+
* RSA key generation and database setup automatically.
|
|
933
510
|
*
|
|
934
511
|
* @example
|
|
935
|
-
*
|
|
512
|
+
* // Server mode
|
|
513
|
+
* const auth = createAuth({
|
|
514
|
+
* mode: 'server',
|
|
515
|
+
* dialect: new PostgresDialect({ pool: new Pool({ connectionString: DATABASE_URL }) }),
|
|
936
516
|
* secret: process.env.JWT_SECRET!,
|
|
937
|
-
* validRoles: ['user', 'admin'
|
|
938
|
-
* adapter: myAdapter,
|
|
517
|
+
* validRoles: ['user', 'admin'] as const,
|
|
939
518
|
* });
|
|
940
519
|
*
|
|
941
|
-
*
|
|
520
|
+
* @example
|
|
521
|
+
* // Client mode
|
|
522
|
+
* const auth = createAuth({
|
|
523
|
+
* mode: 'client',
|
|
524
|
+
* keyUri: 'https://auth.myapp.com/auth/keys',
|
|
525
|
+
* });
|
|
942
526
|
*/
|
|
943
|
-
declare function createAuth<TRole extends string = string>(config:
|
|
527
|
+
declare function createAuth<TRole extends string = string>(config: ServerAuthConfig<TRole>): ServerAuthClient<TRole>;
|
|
528
|
+
declare function createAuth<TRole extends string = string>(config: ClientAuthConfig<TRole>): ClientAuthClient<TRole>;
|
|
944
529
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
530
|
+
type PostgresConfig = {
|
|
531
|
+
connectionString: string;
|
|
532
|
+
max?: number;
|
|
533
|
+
} | {
|
|
534
|
+
host?: string;
|
|
535
|
+
port?: number;
|
|
536
|
+
database: string;
|
|
537
|
+
user: string;
|
|
538
|
+
password: string;
|
|
539
|
+
max?: number;
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
interface CreateServerOptions<TRole extends string = string> {
|
|
543
|
+
validRoles: readonly TRole[];
|
|
544
|
+
db: PostgresConfig;
|
|
545
|
+
accessExpiresIn?: string | number;
|
|
546
|
+
refreshExpiresIn?: string | number;
|
|
547
|
+
saltRounds?: number;
|
|
548
|
+
apiKey?: string;
|
|
549
|
+
cookie?: CookieConfig;
|
|
550
|
+
accessCookie?: AccessCookieConfig;
|
|
551
|
+
hooks?: AuthHooks;
|
|
552
|
+
router?: RouterHandlers;
|
|
553
|
+
isTokenRevoked?: (sessionId: string) => boolean | Promise<boolean>;
|
|
964
554
|
/**
|
|
965
|
-
*
|
|
966
|
-
*
|
|
967
|
-
* prevents unbounded memory growth under high traffic with unique keys.
|
|
968
|
-
* @default 10_000
|
|
555
|
+
* Redis connection URL (e.g. `redis://localhost:6379`).
|
|
556
|
+
* When set, `auth.idempotencyMiddleware()` uses Redis as the cache backend.
|
|
969
557
|
*/
|
|
970
|
-
|
|
558
|
+
redisUrl?: string;
|
|
971
559
|
}
|
|
972
560
|
/**
|
|
973
|
-
*
|
|
561
|
+
* Create a Sentri auth server for PostgreSQL.
|
|
974
562
|
*
|
|
975
|
-
*
|
|
976
|
-
*
|
|
977
|
-
*
|
|
978
|
-
*
|
|
979
|
-
* - **Cache hit**: replies immediately with the stored response and sets
|
|
980
|
-
* `X-Idempotent-Replayed: true` — no handler runs.
|
|
981
|
-
* - **Cache miss**: intercepts `response.json()` to store the result after the
|
|
982
|
-
* handler completes (only 2xx responses are cached).
|
|
983
|
-
*
|
|
984
|
-
* The cache is per-process and in-memory with a configurable TTL. It is suitable
|
|
985
|
-
* for protecting create/update operations against duplicate submissions (network
|
|
986
|
-
* retries, double-clicks) within the same server process. For multi-process
|
|
987
|
-
* deployments consider using a shared cache such as Redis.
|
|
988
|
-
*
|
|
989
|
-
* Requests without the idempotency-key header, or using non-mutating methods, pass
|
|
990
|
-
* straight through to the next middleware.
|
|
563
|
+
* Convenience wrapper over `createAuth()` that:
|
|
564
|
+
* - Accepts plain PostgreSQL connection params instead of a Kysely dialect
|
|
565
|
+
* - Generates an RSA-2048 key pair at startup (RS256, ephemeral per process)
|
|
566
|
+
* - Exposes `GET /keys` (JWKS) automatically for SSO with client-mode apps
|
|
991
567
|
*
|
|
992
568
|
* @example
|
|
993
|
-
*
|
|
994
|
-
*
|
|
995
|
-
*
|
|
996
|
-
*
|
|
997
|
-
*
|
|
998
|
-
*
|
|
999
|
-
* apiRouter.use(createIdempotencyMiddleware({ ttl: 60_000 }));
|
|
1000
|
-
*/
|
|
1001
|
-
declare function createIdempotencyMiddleware(options?: IdempotencyOptions): RequestHandler;
|
|
1002
|
-
|
|
1003
|
-
/**
|
|
1004
|
-
* Extract the access token from an incoming Express request.
|
|
1005
|
-
*
|
|
1006
|
-
* Looks in two places, in order:
|
|
1007
|
-
* 1. `Authorization: Bearer <token>` header
|
|
1008
|
-
* 2. The access token cookie (name from `config.accessCookie.name`, default `'access_token'`)
|
|
1009
|
-
*
|
|
1010
|
-
* Returns `undefined` when neither source is present.
|
|
1011
|
-
*
|
|
1012
|
-
* This is the server-side companion to reading `document.cookie` on the client.
|
|
1013
|
-
* Use it when you need the raw token string for custom middleware or logging.
|
|
569
|
+
* const auth = createAuthServer({
|
|
570
|
+
* validRoles: ['user', 'admin'] as const,
|
|
571
|
+
* db: { connectionString: process.env.DATABASE_URL! },
|
|
572
|
+
* });
|
|
573
|
+
* await auth.migrate();
|
|
574
|
+
* app.use('/auth', auth.router());
|
|
1014
575
|
*
|
|
1015
576
|
* @example
|
|
1016
|
-
*
|
|
1017
|
-
*
|
|
1018
|
-
*
|
|
1019
|
-
*
|
|
1020
|
-
*
|
|
577
|
+
* // With Redis idempotency cache (multi-process deployments)
|
|
578
|
+
* const auth = createAuthServer({
|
|
579
|
+
* validRoles: ['user', 'admin'] as const,
|
|
580
|
+
* db: { connectionString: process.env.DATABASE_URL! },
|
|
581
|
+
* redisUrl: process.env.REDIS_URL,
|
|
1021
582
|
* });
|
|
583
|
+
* app.use(auth.idempotencyMiddleware());
|
|
1022
584
|
*/
|
|
1023
|
-
declare function
|
|
585
|
+
declare function createAuthServer<TRole extends string = string>(options: CreateServerOptions<TRole>): ServerAuthClient<TRole>;
|
|
1024
586
|
|
|
1025
587
|
/**
|
|
1026
|
-
*
|
|
1027
|
-
*
|
|
1028
|
-
*
|
|
1029
|
-
*
|
|
1030
|
-
* the adapter, and returns the created user.
|
|
1031
|
-
*
|
|
1032
|
-
* No tokens are issued — the caller should invoke `login` after registration
|
|
1033
|
-
* if immediate authentication is desired.
|
|
1034
|
-
*
|
|
1035
|
-
* @param input - Registration data: identifier, plain-text password, and optional roles.
|
|
1036
|
-
* @param config - Auth configuration containing the adapter and role definitions.
|
|
1037
|
-
* @returns `{ success: true, user }` on success, or `{ success: false, error }` with
|
|
1038
|
-
* code `INVALID_ROLE` or `USER_ALREADY_EXISTS` on failure.
|
|
588
|
+
* Extract the raw access token string from an Express request.
|
|
589
|
+
* Reads `Authorization: Bearer <token>` header first; falls back to the
|
|
590
|
+
* `access_token` cookie (or the name set in `accessCookie.name`).
|
|
591
|
+
* Returns `undefined` when no token is present.
|
|
1039
592
|
*/
|
|
1040
|
-
declare function
|
|
593
|
+
declare function getCurrentAccessToken(request: Request, config: ServerAuthConfig): string | undefined;
|
|
594
|
+
|
|
595
|
+
declare function register(input: RegisterInput, config: ServerAuthConfig): Promise<RegisterResult>;
|
|
1041
596
|
|
|
1042
597
|
declare global {
|
|
1043
598
|
namespace Express {
|
|
1044
599
|
interface Request {
|
|
1045
|
-
/** Decoded user payload injected by `protect()` on every authenticated request. */
|
|
1046
600
|
user?: AuthUser;
|
|
1047
|
-
/** Idempotency key from `X-Idempotency-Key` header, attached by `createIdempotencyMiddleware()`. */
|
|
1048
601
|
requestId?: string;
|
|
1049
602
|
}
|
|
1050
603
|
}
|
|
1051
604
|
}
|
|
1052
605
|
|
|
1053
|
-
export {
|
|
606
|
+
export { type AccessCookieConfig, type ApiResponse, type AssignRolesResult, type AuthClient, type AuthConfig, type AuthHooks, type AuthResult, type AuthUser, type BulkIdentifiersResult, type ChangePasswordResult, type ChangePrimaryResult, type ClientAuthClient, type ClientAuthConfig, type CookieConfig, type CreateServerOptions, type ErrorHandlerOptions, type GetUserResult, type IdempotencyOptions, type IdentifierInput, type IdentifierRecord, type LoginInput, type PermitCheck, type PermitOptions, type PostgresConfig, type RefreshResult, type RegisterInput, type RegisterResult, type RouterHandlers, SENTRI_ERROR_STATUS, SentriError, type SentriErrorCode, type ServerAuthClient, type ServerAuthConfig, createAuth, createAuthServer, createErrorHandler, createIdempotencyMiddleware, getCurrentAccessToken, register };
|