sentri 1.1.0 → 1.1.2

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.
Files changed (46) hide show
  1. package/README.md +107 -43
  2. package/dist/client.d.ts +49 -14
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +3 -1
  5. package/dist/client.js.map +1 -1
  6. package/dist/errors/AuthError.d.ts +82 -24
  7. package/dist/errors/AuthError.d.ts.map +1 -1
  8. package/dist/errors/AuthError.js +87 -20
  9. package/dist/errors/AuthError.js.map +1 -1
  10. package/dist/index.d.ts +6 -3
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +3 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/libs/config.d.ts +2 -2
  15. package/dist/libs/config.js +8 -8
  16. package/dist/libs/config.js.map +1 -1
  17. package/dist/libs/token.d.ts +2 -2
  18. package/dist/libs/token.js +10 -10
  19. package/dist/libs/token.js.map +1 -1
  20. package/dist/middleware/authorize.d.ts +1 -1
  21. package/dist/middleware/authorize.js +4 -4
  22. package/dist/middleware/authorize.js.map +1 -1
  23. package/dist/middleware/errorHandler.d.ts +71 -0
  24. package/dist/middleware/errorHandler.d.ts.map +1 -0
  25. package/dist/middleware/errorHandler.js +74 -0
  26. package/dist/middleware/errorHandler.js.map +1 -0
  27. package/dist/middleware/permit.d.ts +1 -1
  28. package/dist/middleware/permit.js +4 -4
  29. package/dist/middleware/permit.js.map +1 -1
  30. package/dist/middleware/protect.d.ts +1 -1
  31. package/dist/middleware/protect.js +4 -4
  32. package/dist/middleware/protect.js.map +1 -1
  33. package/dist/middleware/router.d.ts.map +1 -1
  34. package/dist/middleware/router.js +11 -13
  35. package/dist/middleware/router.js.map +1 -1
  36. package/dist/services/auth.d.ts +5 -5
  37. package/dist/services/auth.d.ts.map +1 -1
  38. package/dist/services/auth.js +15 -15
  39. package/dist/services/auth.js.map +1 -1
  40. package/dist/types/auth.d.ts +21 -21
  41. package/dist/types/auth.d.ts.map +1 -1
  42. package/dist/types/auth.js +1 -1
  43. package/dist/types/auth.js.map +1 -1
  44. package/package.json +1 -1
  45. package/templates/drizzle/auth.ts +37 -2
  46. package/templates/prisma/auth.ts +37 -2
package/README.md CHANGED
@@ -17,6 +17,7 @@ Auth and authorization library for Express + PostgreSQL. Provides JWT-based auth
17
17
  - [Programmatic API](#programmatic-api)
18
18
  - [Types](#types)
19
19
  - [Error Handling](#error-handling)
20
+ - [Migration from 1.0.x](#migration-from-10x)
20
21
 
21
22
  ---
22
23
 
@@ -55,7 +56,7 @@ prisma/
55
56
  schema.prisma ← Prisma models (Prisma only, created or appended)
56
57
  ```
57
58
 
58
- ### 2. Mount the router
59
+ ### 2. Mount the router and error handler
59
60
 
60
61
  ```typescript
61
62
  import express from 'express';
@@ -64,6 +65,12 @@ import { auth } from './lib/sentri/auth.js';
64
65
  const app = express();
65
66
  app.use(express.json());
66
67
  app.use('/auth', auth.router());
68
+
69
+ // ... your routes ...
70
+
71
+ // Must be last — catches SentriError from sentri and your own subclasses
72
+ app.use(auth.errorHandler());
73
+ app.listen(3000);
67
74
  ```
68
75
 
69
76
  Done. All endpoints are available at `/auth/*`.
@@ -189,7 +196,7 @@ The `router` field in config lets you replace the built-in service logic for ind
189
196
  Each key is optional — only override what you need. Any key you omit falls back to the built-in behaviour.
190
197
 
191
198
  ```typescript
192
- import { createAuth, AuthError } from 'sentri';
199
+ import { createAuth, SentriError } from 'sentri';
193
200
  import type { AuthResult } from 'sentri';
194
201
 
195
202
  export const auth = createAuth({
@@ -202,7 +209,7 @@ export const auth = createAuth({
202
209
  login: async (input): Promise<AuthResult> => {
203
210
  const otpVerified = await redis.get(`otp:${input.identifier}`);
204
211
  if (!otpVerified) {
205
- return { success: false, error: new AuthError('INVALID_CREDENTIALS', 'OTP required') };
212
+ return { success: false, error: new SentriError('INVALID_CREDENTIALS', 'OTP required') };
206
213
  }
207
214
  return defaultLogin(input);
208
215
  },
@@ -232,7 +239,7 @@ export const auth = createAuth({
232
239
 
233
240
  | Key | Signature | Must return |
234
241
  |---|---|---|
235
- | `register` | `(input: SignupInput) => Promise<SignupResult>` | `SignupResult` |
242
+ | `register` | `(input: RegisterInput) => Promise<RegisterResult>` | `RegisterResult` |
236
243
  | `login` | `(input: LoginInput) => Promise<AuthResult>` | `AuthResult` |
237
244
  | `refresh` | `(refreshToken: string) => Promise<RefreshResult>` | `RefreshResult` |
238
245
  | `logout` | `(refreshToken: string \| undefined) => Promise<void>` | `void` |
@@ -287,7 +294,7 @@ import { createAdapter } from './adapter.js';
287
294
  export const adapter = createAdapter(db);
288
295
  ```
289
296
 
290
- `createAdapter` throws `AuthError` with code `CONFIGURATION_ERROR` at runtime if called without a `db` argument.
297
+ `createAdapter` throws `SentriError` with code `CONFIGURATION_ERROR` at runtime if called without a `db` argument.
291
298
 
292
299
  ---
293
300
 
@@ -473,8 +480,8 @@ Token and password utilities are available on the auth client for use outside th
473
480
  ```typescript
474
481
  // Token utilities
475
482
  const accessToken = auth.signAccessToken({ id, identifier, roles });
476
- const user = auth.verifyAccessToken(accessToken); // throws AuthError if invalid
477
- const { sessionId } = auth.verifyRefreshToken(token); // throws AuthError if invalid
483
+ const user = auth.verifyAccessToken(accessToken); // throws SentriError if invalid
484
+ const { sessionId } = auth.verifyRefreshToken(token); // throws SentriError if invalid
478
485
  const refreshToken = auth.signRefreshToken(sessionId);
479
486
 
480
487
  // Password utilities
@@ -482,7 +489,26 @@ const hash = await auth.hashPassword('secret123');
482
489
  const valid = await auth.verifyPassword('secret123', hash);
483
490
  ```
484
491
 
485
- `verifyAccessToken` and `verifyRefreshToken` throw `AuthError` with code `TOKEN_EXPIRED` or `TOKEN_INVALID` — wrap them in a try/catch or use the router which handles this automatically.
492
+ `verifyAccessToken` and `verifyRefreshToken` throw `SentriError` with code `TOKEN_EXPIRED` or `TOKEN_INVALID` — wrap them in a try/catch or use the router which handles this automatically.
493
+
494
+ ### `register` — standalone service function
495
+
496
+ The `register` function is also exported directly so you can call it outside the built-in router (e.g. in tests, scripts, or admin tools):
497
+
498
+ ```typescript
499
+ import { register } from 'sentri';
500
+
501
+ const result = await register(
502
+ { identifier: 'alice@example.com', password: 'hunter2', roles: ['user'] },
503
+ config,
504
+ );
505
+
506
+ if (result.success) {
507
+ console.log(result.user.id);
508
+ } else {
509
+ console.error(result.error.code); // 'USER_ALREADY_EXISTS' | 'INVALID_ROLE'
510
+ }
511
+ ```
486
512
 
487
513
  ---
488
514
 
@@ -495,11 +521,11 @@ import type {
495
521
  AuthAdapter,
496
522
  AuthUser,
497
523
  AuthResult,
498
- SignupResult,
524
+ RegisterResult,
499
525
  AssignRolesResult,
500
526
  RefreshResult,
501
527
  ApiResponse,
502
- SignupInput,
528
+ RegisterInput,
503
529
  LoginInput,
504
530
  RouterHandlers,
505
531
  UserRecord,
@@ -508,17 +534,17 @@ import type {
508
534
  CookieConfig,
509
535
  PermitCheck,
510
536
  PermitOptions,
511
- AuthErrorCode,
537
+ SentriErrorCode,
512
538
  } from 'sentri';
513
539
 
514
- import { AuthError, createAuth } from 'sentri';
540
+ import { SentriError, AUTH_ERROR_STATUS, createAuth, register } from 'sentri';
515
541
  ```
516
542
 
517
543
  ---
518
544
 
519
545
  ## Error Handling
520
546
 
521
- All errors thrown by the library are instances of `AuthError` with a machine-readable `code`:
547
+ All errors thrown by the library are instances of `SentriError` with a machine-readable `code` and an HTTP `statusCode`:
522
548
 
523
549
  | Code | HTTP | Meaning |
524
550
  |---|---|---|
@@ -533,44 +559,70 @@ All errors thrown by the library are instances of `AuthError` with a machine-rea
533
559
  | `VALIDATION_ERROR` | 400 | Missing or invalid input field |
534
560
  | `CONFIGURATION_ERROR` | 500 | Invalid `createAuth` configuration |
535
561
 
536
- The built-in router converts all `AuthError` instances to the standard envelope automatically. For custom routes:
562
+ ### `auth.errorHandler()`
563
+
564
+ Mount `auth.errorHandler()` **after all your routes** to automatically format every `SentriError` (and any subclass) into the standard envelope:
537
565
 
538
566
  ```typescript
539
- import { AuthError } from 'sentri';
540
-
541
- const AUTH_ERROR_STATUS: Record<string, number> = {
542
- UNAUTHORIZED: 401,
543
- TOKEN_EXPIRED: 401,
544
- TOKEN_INVALID: 401,
545
- INVALID_CREDENTIALS: 401,
546
- FORBIDDEN: 403,
547
- USER_NOT_FOUND: 404,
548
- USER_ALREADY_EXISTS: 409,
549
- INVALID_ROLE: 400,
550
- VALIDATION_ERROR: 400,
551
- CONFIGURATION_ERROR: 500,
552
- };
567
+ app.use('/auth', auth.router());
568
+ app.use('/api', apiRouter);
569
+
570
+ // Must be last
571
+ app.use(auth.errorHandler());
572
+ ```
573
+
574
+ Optional logger for unexpected errors:
575
+
576
+ ```typescript
577
+ app.use(auth.errorHandler({
578
+ onUnhandled: (err) => logger.error('Unexpected error', { err }),
579
+ }));
580
+ ```
581
+
582
+ ### Extending `SentriError`
583
+
584
+ Define application-specific errors by extending `SentriError`. They are caught automatically by `auth.errorHandler()` via `instanceof`:
585
+
586
+ ```typescript
587
+ import { SentriError } from 'sentri';
588
+
589
+ export class NotFoundError extends SentriError {
590
+ constructor(resource: string) {
591
+ super('NOT_FOUND', `${resource} not found`, 404);
592
+ }
593
+ }
553
594
 
554
- app.use((error, _request, response, next) => {
555
- if (error instanceof AuthError) {
556
- const statusCode = AUTH_ERROR_STATUS[error.code] ?? 500;
557
- return response.status(statusCode).json({
558
- error: true,
559
- statusCode,
560
- code: error.code,
561
- message: error.message,
562
- data: null,
563
- });
595
+ export class PaymentError extends SentriError {
596
+ constructor(message: string) {
597
+ super('PAYMENT_FAILED', message, 402);
564
598
  }
565
- next(error);
599
+ }
600
+
601
+ // Throw anywhere in your routes — auth.errorHandler() catches them all
602
+ app.get('/items/:id', auth.protect(), async (req, res) => {
603
+ const item = await db.items.findById(req.params['id']);
604
+ if (!item) throw new NotFoundError('Item');
605
+ res.json(item);
566
606
  });
567
607
  ```
568
608
 
609
+ Response shape for any `SentriError` (built-in or custom):
610
+
611
+ ```json
612
+ {
613
+ "error": true,
614
+ "statusCode": 404,
615
+ "code": "NOT_FOUND",
616
+ "message": "Item not found",
617
+ "data": null
618
+ }
619
+ ```
620
+
569
621
  ---
570
622
 
571
623
  ## Migration from 1.0.x
572
624
 
573
- ### Breaking changes
625
+ ### Breaking changes in 1.1.0
574
626
 
575
627
  | What changed | Action required |
576
628
  |---|---|
@@ -578,7 +630,19 @@ app.use((error, _request, response, next) => {
578
630
  | `RouterHandlers.signup` renamed to `RouterHandlers.register` | Update config if you used a custom signup handler |
579
631
  | `protect()` now performs one DB read per request | Ensure your adapter's `session.findById` is indexed on session ID |
580
632
 
581
- ### New features
633
+ ### Breaking changes in 1.2.0
582
634
 
583
- - **Session-bound tokens** access tokens are immediately invalidated after logout without waiting for expiry.
584
- - **`apiKey` config** — lock `POST /register` to trusted callers via `X-Api-Key` header.
635
+ | What changed | Action required |
636
+ |---|---|
637
+ | `AuthError` renamed to `SentriError` | Replace all `import { AuthError }` with `import { SentriError }` |
638
+ | `AuthErrorCode` renamed to `SentriErrorCode` | Replace all `AuthErrorCode` type references |
639
+ | `SignupResult` renamed to `RegisterResult` | Replace type references |
640
+ | `SignupInput` renamed to `RegisterInput` | Replace type references |
641
+ | `signup` service renamed to `register` | Replace `import { signup }` with `import { register }` |
642
+
643
+ ### New features in 1.2.0
644
+
645
+ - **`auth.errorHandler()`** — built-in Express error handler mounted like `auth.router()`.
646
+ - **`SentriError.statusCode`** — each error carries its own HTTP status; no need for a manual status map.
647
+ - **Extensible errors** — subclass `SentriError` with a custom `code` and `statusCode`; `auth.errorHandler()` catches all subclasses automatically.
648
+ - **`register` exported** — the registration service function is now a named export for use outside the built-in router.
package/dist/client.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { PermitCheck, PermitOptions } from './middleware/permit.js';
2
+ import type { ErrorHandlerOptions } from './middleware/errorHandler.js';
2
3
  import type { AuthConfig, AuthUser } from './types/auth.js';
3
- import type { RequestHandler, Router } from 'express';
4
+ import type { ErrorRequestHandler, RequestHandler, Router } from 'express';
4
5
  /**
5
6
  * The bound auth client returned by {@link createAuth}.
6
7
  *
@@ -15,7 +16,8 @@ export interface AuthClient<TRole extends string = string> {
15
16
  * Express middleware factory that enforces authentication.
16
17
  *
17
18
  * Reads the `Authorization: Bearer <token>` header, verifies the access token,
18
- * and injects the decoded payload as `request.user`. Calls `next(AuthError)` on failure.
19
+ * confirms the session is still active in the database, and injects the decoded
20
+ * payload as `request.user`. Calls `next(SentriError)` on any failure.
19
21
  *
20
22
  * @example
21
23
  * router.get('/me', auth.protect(), (request, response) => {
@@ -27,7 +29,7 @@ export interface AuthClient<TRole extends string = string> {
27
29
  * Express middleware factory that enforces role-based access.
28
30
  *
29
31
  * Must be used **after** `protect()`. Passes if the authenticated user has
30
- * at least one of the specified roles; otherwise calls `next(AuthError)` with
32
+ * at least one of the specified roles; otherwise calls `next(SentriError)` with
31
33
  * code `FORBIDDEN`.
32
34
  *
33
35
  * @example
@@ -38,7 +40,7 @@ export interface AuthClient<TRole extends string = string> {
38
40
  * Express middleware factory for resource-level permission checks.
39
41
  *
40
42
  * Must be used **after** `protect()`. Evaluates a check function against the
41
- * current request and calls `next(AuthError)` with `FORBIDDEN` if it returns `false`.
43
+ * current request and calls `next(SentriError)` with `FORBIDDEN` if it returns `false`.
42
44
  *
43
45
  * Accepts either a bare check function or an options object with an optional
44
46
  * `roles` list whose members bypass the check entirely.
@@ -75,32 +77,65 @@ export interface AuthClient<TRole extends string = string> {
75
77
  signAccessToken(payload: AuthUser<TRole>): string;
76
78
  /** Sign a refresh token bound to a session ID. */
77
79
  signRefreshToken(sessionId: string): string;
78
- /** Verify and decode an access token. Throws `AuthError` if invalid or expired. */
80
+ /** Verify and decode an access token. Throws `SentriError` if invalid or expired. */
79
81
  verifyAccessToken(token: string): AuthUser<TRole>;
80
- /** Verify and decode a refresh token. Throws `AuthError` if invalid or expired. */
82
+ /** Verify and decode a refresh token. Throws `SentriError` if invalid or expired. */
81
83
  verifyRefreshToken(token: string): {
82
84
  sessionId: string;
83
85
  };
84
86
  /**
85
- * Returns a pre-built Express Router with all standard auth endpoints mounted:
87
+ * Returns a pre-built Express Router with all standard auth endpoints mounted.
86
88
  *
87
- * - `POST /register` — register a new user, returns `{ user }`.
88
- * Requires `X-Api-Key` header when `config.apiKey` is set.
89
+ * Endpoints:
90
+ * - `POST /register` — register a new user. Requires `X-Api-Key` header when `config.apiKey` is set.
89
91
  * - `POST /login` — authenticate, sets refresh token cookie, returns `{ accessToken, user }`
90
- * - `POST /refresh` — reads refresh token from cookie, returns `{ accessToken }`
91
- * - `POST /logout` — invalidate the current session; any access token bound to
92
- * this session is immediately rejected by `protect()`.
93
- * - `POST /logout-all` — invalidate all sessions for the user (requires valid access token)
92
+ * - `POST /refresh` — rotate refresh token, returns new `{ accessToken }`
93
+ * - `POST /logout` — delete the current session; the bound access token is immediately rejected by `protect()`
94
+ * - `POST /logout-all` delete all sessions for the user (requires valid access token)
94
95
  * - `GET /me` — return the authenticated user
95
96
  * - `POST /users/:userId/roles` — assign roles (requires admin)
96
97
  *
97
- * Requires `express.json()` to be applied before the router.
98
+ * Requires `express.json()` before the router.
98
99
  *
99
100
  * @example
100
101
  * app.use(express.json());
101
102
  * app.use('/auth', auth.router());
102
103
  */
103
104
  router(): Router;
105
+ /**
106
+ * Returns an Express error-handling middleware that formats every `SentriError`
107
+ * (and any subclass) into the standard sentri response envelope:
108
+ *
109
+ * ```json
110
+ * { "error": true, "statusCode": 401, "code": "UNAUTHORIZED", "message": "...", "data": null }
111
+ * ```
112
+ *
113
+ * Mount it **after all your routes** so it acts as the global catch-all for
114
+ * both sentri errors and your own `SentriError` subclasses.
115
+ *
116
+ * @example
117
+ * import { SentriError } from 'sentri';
118
+ *
119
+ * // Define app-specific errors by extending SentriError
120
+ * class NotFoundError extends SentriError {
121
+ * constructor(resource: string) {
122
+ * super('NOT_FOUND', `${resource} not found`, 404);
123
+ * }
124
+ * }
125
+ *
126
+ * app.use('/auth', auth.router());
127
+ * app.use('/api', apiRouter);
128
+ *
129
+ * // Catches errors from sentri AND your own subclasses
130
+ * app.use(auth.errorHandler());
131
+ *
132
+ * @example
133
+ * // With optional unhandled-error logger
134
+ * app.use(auth.errorHandler({
135
+ * onUnhandled: (err) => logger.error('Unexpected error', { err }),
136
+ * }));
137
+ */
138
+ errorHandler(options?: ErrorHandlerOptions): ErrorRequestHandler;
104
139
  }
105
140
  /**
106
141
  * Create a fully configured auth client for your application.
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEtD;;;;;;;;GAQG;AACH,MAAM,WAAW,UAAU,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM;IACvD;;;;;;;;;;OAUG;IACH,OAAO,IAAI,cAAc,CAAC;IAE1B;;;;;;;;;OASG;IACH,SAAS,CAAC,GAAG,KAAK,EAAE,KAAK,EAAE,GAAG,cAAc,CAAC;IAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8BG;IACH,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,cAAc,CAAC;IAC3C,MAAM,CAAC,OAAO,EAAE,aAAa,CAAC,KAAK,CAAC,GAAG,cAAc,CAAC;IAEtD,oEAAoE;IACpE,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAE7C,kEAAkE;IAClE,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE9D,uDAAuD;IACvD,eAAe,CAAC,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC;IAElD,kDAAkD;IAClD,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IAE5C,mFAAmF;IACnF,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAElD,mFAAmF;IACnF,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAEzD;;;;;;;;;;;;;;;;;;OAkBG;IACH,MAAM,IAAI,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,UAAU,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,EACtD,MAAM,EAAE,UAAU,CAAC,KAAK,CAAC,GACxB,UAAU,CAAC,KAAK,CAAC,CAgBnB"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAE3E;;;;;;;;GAQG;AACH,MAAM,WAAW,UAAU,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM;IACvD;;;;;;;;;;;OAWG;IACH,OAAO,IAAI,cAAc,CAAC;IAE1B;;;;;;;;;OASG;IACH,SAAS,CAAC,GAAG,KAAK,EAAE,KAAK,EAAE,GAAG,cAAc,CAAC;IAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8BG;IACH,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,cAAc,CAAC;IAC3C,MAAM,CAAC,OAAO,EAAE,aAAa,CAAC,KAAK,CAAC,GAAG,cAAc,CAAC;IAEtD,oEAAoE;IACpE,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAE7C,kEAAkE;IAClE,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE9D,uDAAuD;IACvD,eAAe,CAAC,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC;IAElD,kDAAkD;IAClD,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IAE5C,qFAAqF;IACrF,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAElD,qFAAqF;IACrF,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAEzD;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,IAAI,MAAM,CAAC;IAEjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgCG;IACH,YAAY,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,mBAAmB,CAAC;CAClE;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,UAAU,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,EACtD,MAAM,EAAE,UAAU,CAAC,KAAK,CAAC,GACxB,UAAU,CAAC,KAAK,CAAC,CAiBnB"}
package/dist/client.js CHANGED
@@ -5,6 +5,7 @@ import { protect } from './middleware/protect.js';
5
5
  import { authorize } from './middleware/authorize.js';
6
6
  import { permit } from './middleware/permit.js';
7
7
  import { createAuthRouter } from './middleware/router.js';
8
+ import { createErrorHandler } from './middleware/errorHandler.js';
8
9
  /**
9
10
  * Create a fully configured auth client for your application.
10
11
  *
@@ -30,6 +31,7 @@ export function createAuth(config) {
30
31
  return {
31
32
  protect: () => protect(config),
32
33
  authorize: (...roles) => authorize(...roles),
34
+ permit: (optionsOrCheck) => permit(optionsOrCheck),
33
35
  hashPassword: (plain) => hashPassword(plain, resolved.saltRounds),
34
36
  verifyPassword: (plain, hash) => verifyPassword(plain, hash),
35
37
  signAccessToken: (payload) => signAccessToken(payload, config),
@@ -37,7 +39,7 @@ export function createAuth(config) {
37
39
  verifyAccessToken: (token) => verifyAccessToken(token, config),
38
40
  verifyRefreshToken: (token) => verifyRefreshToken(token, config),
39
41
  router: () => createAuthRouter(config),
40
- permit: (optionsOrCheck) => permit(optionsOrCheck),
42
+ errorHandler: (options) => createErrorHandler(options),
41
43
  };
42
44
  }
43
45
  //# sourceMappingURL=client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC3G,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAChD,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAkH1D;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,UAAU,CACxB,MAAyB;IAEzB,cAAc,CAAC,MAAoB,CAAC,CAAC;IACrC,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAoB,CAAC,CAAC;IAErD,OAAO;QACL,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAoB,CAAC;QAC5C,SAAS,EAAE,CAAC,GAAG,KAAK,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,KAAK,CAAC;QAC5C,YAAY,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC,UAAU,CAAC;QACjE,cAAc,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,IAAI,CAAC;QAC5D,eAAe,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,eAAe,CAAC,OAAmB,EAAE,MAAoB,CAAC;QACxF,gBAAgB,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,gBAAgB,CAAC,SAAS,EAAE,MAAoB,CAAC;QAClF,iBAAiB,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,iBAAiB,CAAC,KAAK,EAAE,MAAoB,CAAoB;QAC/F,kBAAkB,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,kBAAkB,CAAC,KAAK,EAAE,MAAoB,CAAC;QAC9E,MAAM,EAAE,GAAG,EAAE,CAAC,gBAAgB,CAAC,MAAM,CAAC;QACtC,MAAM,EAAE,CAAC,cAAkD,EAAE,EAAE,CAAC,MAAM,CAAC,cAAc,CAAC;KACvF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC3G,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAChD,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAsJlE;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,UAAU,CACxB,MAAyB;IAEzB,cAAc,CAAC,MAAoB,CAAC,CAAC;IACrC,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAoB,CAAC,CAAC;IAErD,OAAO;QACL,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAoB,CAAC;QAC5C,SAAS,EAAE,CAAC,GAAG,KAAK,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,KAAK,CAAC;QAC5C,MAAM,EAAE,CAAC,cAAkD,EAAE,EAAE,CAAC,MAAM,CAAC,cAAc,CAAC;QACtF,YAAY,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC,UAAU,CAAC;QACjE,cAAc,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,IAAI,CAAC;QAC5D,eAAe,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,eAAe,CAAC,OAAmB,EAAE,MAAoB,CAAC;QACxF,gBAAgB,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,gBAAgB,CAAC,SAAS,EAAE,MAAoB,CAAC;QAClF,iBAAiB,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,iBAAiB,CAAC,KAAK,EAAE,MAAoB,CAAoB;QAC/F,kBAAkB,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,kBAAkB,CAAC,KAAK,EAAE,MAAoB,CAAC;QAC9E,MAAM,EAAE,GAAG,EAAE,CAAC,gBAAgB,CAAC,MAAM,CAAC;QACtC,YAAY,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,kBAAkB,CAAC,OAAO,CAAC;KACvD,CAAC;AACJ,CAAC"}
@@ -1,41 +1,99 @@
1
1
  /**
2
- * Discriminant codes for {@link AuthError}.
2
+ * Discriminant codes for built-in {@link SentriError} instances.
3
3
  *
4
4
  * - `INVALID_CREDENTIALS` — identifier or password did not match (intentionally vague to prevent user enumeration)
5
5
  * - `USER_NOT_FOUND` — an operation required a user that does not exist
6
- * - `USER_ALREADY_EXISTS` — signup was attempted with an identifier already in the database
6
+ * - `USER_ALREADY_EXISTS` — registration was attempted with an identifier already in the database
7
7
  * - `TOKEN_EXPIRED` — the JWT was valid but its `exp` claim is in the past
8
8
  * - `TOKEN_INVALID` — the JWT could not be verified (bad signature, malformed, wrong type)
9
9
  * - `FORBIDDEN` — the user is authenticated but lacks the required role
10
- * - `UNAUTHORIZED` — no valid access token was present on the request
10
+ * - `UNAUTHORIZED` — no valid access token was present on the request, or the session was revoked
11
11
  * - `INVALID_ROLE` — a role name was used that is not in `validRoles`
12
12
  * - `VALIDATION_ERROR` — a required field was missing or had an invalid value
13
13
  * - `CONFIGURATION_ERROR` — `createAuth` was called with an invalid configuration
14
+ *
15
+ * When you extend {@link SentriError} for your own error types you can use any
16
+ * string as `code` — it does not need to be one of these built-in values.
17
+ */
18
+ export type SentriErrorCode = 'INVALID_CREDENTIALS' | 'USER_NOT_FOUND' | 'USER_ALREADY_EXISTS' | 'TOKEN_EXPIRED' | 'TOKEN_INVALID' | 'FORBIDDEN' | 'UNAUTHORIZED' | 'INVALID_ROLE' | 'VALIDATION_ERROR' | 'CONFIGURATION_ERROR';
19
+ /**
20
+ * Default HTTP status codes for built-in error codes.
21
+ * Custom codes that are not in this map default to 500.
22
+ *
23
+ * @internal
14
24
  */
15
- export type AuthErrorCode = 'INVALID_CREDENTIALS' | 'USER_NOT_FOUND' | 'USER_ALREADY_EXISTS' | 'TOKEN_EXPIRED' | 'TOKEN_INVALID' | 'FORBIDDEN' | 'UNAUTHORIZED' | 'INVALID_ROLE' | 'VALIDATION_ERROR' | 'CONFIGURATION_ERROR';
25
+ export declare const AUTH_ERROR_STATUS: Record<string, number>;
16
26
  /**
17
- * Error class thrown by the library for all authentication and authorization failures.
18
- *
19
- * Carries a machine-readable `code` that lets you distinguish error types without
20
- * string-matching on the message.
21
- *
22
- * @example
23
- * import { AuthError } from 'sentri';
24
- *
25
- * app.use((error, _request, response, next) => {
26
- * if (error instanceof AuthError) {
27
- * const status =
28
- * error.code === 'UNAUTHORIZED' ? 401
29
- * : error.code === 'FORBIDDEN' ? 403
30
- * : 400;
31
- * response.status(status).json({ error: error.code, message: error.message });
32
- * } else {
33
- * next(error);
27
+ * Base error class for all authentication and authorization failures in sentri.
28
+ *
29
+ * Every error thrown by sentri is an instance of `SentriError`. The `code`
30
+ * property is a machine-readable string that lets you distinguish error
31
+ * types without string-matching on the message. Built-in codes are listed
32
+ * in {@link SentriErrorCode}; custom subclasses may use any string.
33
+ *
34
+ * The `statusCode` property holds the HTTP status that the built-in router
35
+ * and `auth.errorHandler()` will use in the response. For built-in codes
36
+ * it is derived automatically. Pass it explicitly when subclassing with a
37
+ * custom code.
38
+ *
39
+ * ---
40
+ *
41
+ * **Extending SentriError**
42
+ *
43
+ * You can create application-specific error classes by extending `SentriError`.
44
+ * Any subclass will be caught automatically by `auth.errorHandler()` because
45
+ * `instanceof SentriError` is `true` for all subclasses.
46
+ *
47
+ * ```typescript
48
+ * import { SentriError } from 'sentri';
49
+ *
50
+ * // Domain error with a custom code and explicit HTTP status
51
+ * export class PaymentError extends SentriError {
52
+ * constructor(message: string) {
53
+ * super('PAYMENT_FAILED', message, 402);
34
54
  * }
55
+ * }
56
+ *
57
+ * // Throw it anywhere in your routes — auth.errorHandler() catches it
58
+ * router.post('/checkout', auth.protect(), async (req, res) => {
59
+ * const ok = await chargeCard(req.body.cardToken);
60
+ * if (!ok) throw new PaymentError('Card declined');
61
+ * res.json({ success: true });
35
62
  * });
63
+ * ```
64
+ *
65
+ * ---
66
+ *
67
+ * **Error handling in custom routes**
68
+ *
69
+ * ```typescript
70
+ * app.use('/auth', auth.router());
71
+ * app.use('/api', apiRouter);
72
+ *
73
+ * // Mount after all routes — catches SentriError from sentri AND your subclasses
74
+ * app.use(auth.errorHandler());
75
+ * ```
36
76
  */
37
- export declare class AuthError extends Error {
38
- readonly code: AuthErrorCode;
39
- constructor(code: AuthErrorCode, message: string);
77
+ export declare class SentriError extends Error {
78
+ /**
79
+ * Machine-readable error code.
80
+ * Built-in codes are defined by {@link SentriErrorCode}.
81
+ * Custom subclasses may use any string.
82
+ */
83
+ readonly code: string;
84
+ /**
85
+ * HTTP status code associated with this error.
86
+ * Derived automatically for built-in codes; pass it explicitly when
87
+ * subclassing with a custom `code`.
88
+ */
89
+ readonly statusCode: number;
90
+ /**
91
+ * @param code - Machine-readable error code. Use a built-in {@link SentriErrorCode}
92
+ * or any string for custom subclasses.
93
+ * @param message - Human-readable description of the error.
94
+ * @param statusCode - HTTP status to use in the response. For built-in codes
95
+ * this is derived automatically; for custom codes it defaults to `500`.
96
+ */
97
+ constructor(code: SentriErrorCode | (string & {}), message: string, statusCode?: number);
40
98
  }
41
99
  //# sourceMappingURL=AuthError.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AuthError.d.ts","sourceRoot":"","sources":["../../src/errors/AuthError.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,aAAa,GACrB,qBAAqB,GACrB,gBAAgB,GAChB,qBAAqB,GACrB,eAAe,GACf,eAAe,GACf,WAAW,GACX,cAAc,GACd,cAAc,GACd,kBAAkB,GAClB,qBAAqB,CAAC;AAE1B;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,SAAU,SAAQ,KAAK;IAClC,SAAgB,IAAI,EAAE,aAAa,CAAC;gBAExB,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM;CAKjD"}
1
+ {"version":3,"file":"AuthError.d.ts","sourceRoot":"","sources":["../../src/errors/AuthError.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,eAAe,GACvB,qBAAqB,GACrB,gBAAgB,GAChB,qBAAqB,GACrB,eAAe,GACf,eAAe,GACf,WAAW,GACX,cAAc,GACd,cAAc,GACd,kBAAkB,GAClB,qBAAqB,CAAC;AAE1B;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAWpD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC;;;;OAIG;IACH,SAAgB,IAAI,EAAE,MAAM,CAAC;IAE7B;;;;OAIG;IACH,SAAgB,UAAU,EAAE,MAAM,CAAC;IAEnC;;;;;;OAMG;gBAED,IAAI,EAAE,eAAe,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,EACrC,OAAO,EAAE,MAAM,EACf,UAAU,CAAC,EAAE,MAAM;CAOtB"}
@@ -1,30 +1,97 @@
1
1
  /**
2
- * Error class thrown by the library for all authentication and authorization failures.
3
- *
4
- * Carries a machine-readable `code` that lets you distinguish error types without
5
- * string-matching on the message.
6
- *
7
- * @example
8
- * import { AuthError } from 'sentri';
9
- *
10
- * app.use((error, _request, response, next) => {
11
- * if (error instanceof AuthError) {
12
- * const status =
13
- * error.code === 'UNAUTHORIZED' ? 401
14
- * : error.code === 'FORBIDDEN' ? 403
15
- * : 400;
16
- * response.status(status).json({ error: error.code, message: error.message });
17
- * } else {
18
- * next(error);
2
+ * Default HTTP status codes for built-in error codes.
3
+ * Custom codes that are not in this map default to 500.
4
+ *
5
+ * @internal
6
+ */
7
+ export const AUTH_ERROR_STATUS = {
8
+ UNAUTHORIZED: 401,
9
+ TOKEN_EXPIRED: 401,
10
+ TOKEN_INVALID: 401,
11
+ INVALID_CREDENTIALS: 401,
12
+ FORBIDDEN: 403,
13
+ USER_NOT_FOUND: 404,
14
+ USER_ALREADY_EXISTS: 409,
15
+ INVALID_ROLE: 400,
16
+ VALIDATION_ERROR: 400,
17
+ CONFIGURATION_ERROR: 500,
18
+ };
19
+ /**
20
+ * Base error class for all authentication and authorization failures in sentri.
21
+ *
22
+ * Every error thrown by sentri is an instance of `SentriError`. The `code`
23
+ * property is a machine-readable string that lets you distinguish error
24
+ * types without string-matching on the message. Built-in codes are listed
25
+ * in {@link SentriErrorCode}; custom subclasses may use any string.
26
+ *
27
+ * The `statusCode` property holds the HTTP status that the built-in router
28
+ * and `auth.errorHandler()` will use in the response. For built-in codes
29
+ * it is derived automatically. Pass it explicitly when subclassing with a
30
+ * custom code.
31
+ *
32
+ * ---
33
+ *
34
+ * **Extending SentriError**
35
+ *
36
+ * You can create application-specific error classes by extending `SentriError`.
37
+ * Any subclass will be caught automatically by `auth.errorHandler()` because
38
+ * `instanceof SentriError` is `true` for all subclasses.
39
+ *
40
+ * ```typescript
41
+ * import { SentriError } from 'sentri';
42
+ *
43
+ * // Domain error with a custom code and explicit HTTP status
44
+ * export class PaymentError extends SentriError {
45
+ * constructor(message: string) {
46
+ * super('PAYMENT_FAILED', message, 402);
19
47
  * }
48
+ * }
49
+ *
50
+ * // Throw it anywhere in your routes — auth.errorHandler() catches it
51
+ * router.post('/checkout', auth.protect(), async (req, res) => {
52
+ * const ok = await chargeCard(req.body.cardToken);
53
+ * if (!ok) throw new PaymentError('Card declined');
54
+ * res.json({ success: true });
20
55
  * });
56
+ * ```
57
+ *
58
+ * ---
59
+ *
60
+ * **Error handling in custom routes**
61
+ *
62
+ * ```typescript
63
+ * app.use('/auth', auth.router());
64
+ * app.use('/api', apiRouter);
65
+ *
66
+ * // Mount after all routes — catches SentriError from sentri AND your subclasses
67
+ * app.use(auth.errorHandler());
68
+ * ```
21
69
  */
22
- export class AuthError extends Error {
70
+ export class SentriError extends Error {
71
+ /**
72
+ * Machine-readable error code.
73
+ * Built-in codes are defined by {@link SentriErrorCode}.
74
+ * Custom subclasses may use any string.
75
+ */
23
76
  code;
24
- constructor(code, message) {
77
+ /**
78
+ * HTTP status code associated with this error.
79
+ * Derived automatically for built-in codes; pass it explicitly when
80
+ * subclassing with a custom `code`.
81
+ */
82
+ statusCode;
83
+ /**
84
+ * @param code - Machine-readable error code. Use a built-in {@link SentriErrorCode}
85
+ * or any string for custom subclasses.
86
+ * @param message - Human-readable description of the error.
87
+ * @param statusCode - HTTP status to use in the response. For built-in codes
88
+ * this is derived automatically; for custom codes it defaults to `500`.
89
+ */
90
+ constructor(code, message, statusCode) {
25
91
  super(message);
26
- this.name = 'AuthError';
92
+ this.name = 'SentriError';
27
93
  this.code = code;
94
+ this.statusCode = statusCode ?? AUTH_ERROR_STATUS[code] ?? 500;
28
95
  }
29
96
  }
30
97
  //# sourceMappingURL=AuthError.js.map