sentri 2.1.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,6 +14,7 @@ Auth and authorization library for Express. Supports two modes:
14
14
  - [Server Mode](#server-mode)
15
15
  - [Client Mode](#client-mode)
16
16
  - [SSO Flow](#sso-flow)
17
+ - [Multi-Identifier](#multi-identifier)
17
18
  - [Configuration](#configuration)
18
19
  - [Endpoints](#endpoints)
19
20
  - [Middleware](#middleware)
@@ -165,7 +166,8 @@ createAuth({
165
166
  isTokenRevoked: async (sessionId) => await redis.sismember('revoked', sessionId),
166
167
  router: { // override built-in service functions
167
168
  login, register, refresh, logout, logoutAll, assignRoles,
168
- changeIdentifier, changePassword,
169
+ bulkCreateIdentifiers, bulkUpdateIdentifiers, bulkDeleteIdentifiers,
170
+ changePrimaryIdentifier, changePassword,
169
171
  },
170
172
  });
171
173
  ```
@@ -177,9 +179,10 @@ createAuth({
177
179
  await auth.migrate();
178
180
  ```
179
181
 
180
- Creates two tables:
181
- - `sentri_users` — id, identifier, password_hash, roles (JSON), created_at
182
+ Creates three tables:
183
+ - `sentri_users` — id, password_hash, roles (JSON), created_at
182
184
  - `sentri_sessions` — id, user_id, expires_at, created_at
185
+ - `sentri_identifiers` — id, user_id, type, value (globally unique), is_primary, created_at
183
186
 
184
187
  ---
185
188
 
@@ -223,6 +226,143 @@ Client apps point `keyUri` at this endpoint and receive the public key automatic
223
226
 
224
227
  ---
225
228
 
229
+ ## Multi-Identifier
230
+
231
+ Each user can have multiple identifiers — email, username, phone number, or any custom type. All identifier values are **globally unique** regardless of type.
232
+
233
+ One identifier per user is marked as **primary** and its value is embedded in the JWT payload. Regardless of which identifier a user logs in with, the JWT always contains the primary identifier.
234
+
235
+ ### Registration
236
+
237
+ Provide at least one identifier. The first entry becomes the primary.
238
+
239
+ ```
240
+ POST /register
241
+ Content-Type: application/json
242
+
243
+ {
244
+ "identifiers": [
245
+ { "type": "email", "value": "rizz@example.com" },
246
+ { "type": "username", "value": "rizz" }
247
+ ],
248
+ "password": "secret123",
249
+ "roles": ["user"]
250
+ }
251
+ ```
252
+
253
+ **Response:**
254
+ ```json
255
+ {
256
+ "error": false,
257
+ "statusCode": 201,
258
+ "message": "User registered successfully",
259
+ "data": {
260
+ "user": {
261
+ "id": "uuid",
262
+ "identifier": "rizz@example.com",
263
+ "identifierType": "email",
264
+ "roles": ["user"],
265
+ "identifiers": [
266
+ { "id": "uuid-1", "type": "email", "value": "rizz@example.com", "isPrimary": true },
267
+ { "id": "uuid-2", "type": "username", "value": "rizz", "isPrimary": false }
268
+ ]
269
+ }
270
+ }
271
+ }
272
+ ```
273
+
274
+ ### Login
275
+
276
+ Send any of the user's identifier values — Sentri searches all types automatically.
277
+
278
+ ```
279
+ POST /login
280
+ Content-Type: application/json
281
+
282
+ { "identifier": "rizz", "password": "secret123" }
283
+ ```
284
+
285
+ ### Bulk Create Identifiers
286
+
287
+ ```
288
+ POST /me/identifiers
289
+ Authorization: Bearer <token>
290
+ Content-Type: application/json
291
+
292
+ {
293
+ "identifiers": [
294
+ { "type": "phone", "value": "+628123456789" }
295
+ ]
296
+ }
297
+ ```
298
+
299
+ ### Bulk Update Identifiers
300
+
301
+ ```
302
+ PUT /me/identifiers
303
+ Authorization: Bearer <token>
304
+ Content-Type: application/json
305
+
306
+ {
307
+ "identifiers": [
308
+ { "id": "uuid-2", "type": "username", "value": "newrizz" }
309
+ ]
310
+ }
311
+ ```
312
+
313
+ ### Bulk Delete Identifiers
314
+
315
+ ```
316
+ DELETE /me/identifiers
317
+ Authorization: Bearer <token>
318
+ Content-Type: application/json
319
+
320
+ { "ids": ["uuid-2"] }
321
+ ```
322
+
323
+ At least one identifier must remain after deletion.
324
+
325
+ ### Change Primary Identifier
326
+
327
+ ```
328
+ PATCH /me/identifiers/primary
329
+ Authorization: Bearer <token>
330
+ Content-Type: application/json
331
+
332
+ { "id": "uuid-2" }
333
+ ```
334
+
335
+ The new primary value will be embedded in the JWT on the next login or token refresh.
336
+
337
+ ### Programmatic API
338
+
339
+ ```typescript
340
+ const auth = createAuth({ mode: 'server', ... });
341
+
342
+ // Register with multiple identifiers
343
+ await auth.register({
344
+ identifiers: [
345
+ { type: 'email', value: 'rizz@example.com' },
346
+ { type: 'username', value: 'rizz' },
347
+ ],
348
+ password: 'secret123',
349
+ });
350
+
351
+ // Add identifiers after registration
352
+ await auth.bulkCreateIdentifiers(userId, [{ type: 'phone', value: '+628123456789' }]);
353
+
354
+ // Update identifiers
355
+ await auth.bulkUpdateIdentifiers(userId, [{ id: 'uuid-2', type: 'username', value: 'newrizz' }]);
356
+
357
+ // Delete identifiers
358
+ await auth.bulkDeleteIdentifiers(userId, ['uuid-2']);
359
+
360
+ // Change primary
361
+ await auth.changePrimaryIdentifier(userId, 'uuid-3');
362
+ ```
363
+
364
+ ---
365
+
226
366
  ## Configuration
227
367
 
228
368
  ### `algorithm`
@@ -259,8 +399,11 @@ After login, both cookies are set automatically. `protect()` reads the access to
259
399
  | `POST` | `/refresh` | — | Rotate refresh token |
260
400
  | `POST` | `/logout` | — | Invalidate current session |
261
401
  | `POST` | `/logout-all` | ✓ | Invalidate all sessions |
262
- | `GET` | `/me` | ✓ | Return authenticated user |
263
- | `PATCH` | `/me/identifier` | ✓ self | Change identifier (username/email) |
402
+ | `GET` | `/me` | ✓ | Return authenticated user with all identifiers |
403
+ | `POST` | `/me/identifiers` | ✓ self | Add identifiers in bulk |
404
+ | `PUT` | `/me/identifiers` | ✓ self | Update identifiers in bulk |
405
+ | `DELETE` | `/me/identifiers` | ✓ self | Delete identifiers in bulk |
406
+ | `PATCH` | `/me/identifiers/primary` | ✓ self | Change primary identifier |
264
407
  | `PATCH` | `/me/password` | ✓ self | Change password — revokes all sessions |
265
408
  | `POST` | `/users/:userId/roles` | ✓ admin | Assign roles to user |
266
409
  | `GET` | `/keys` | — | Public key in JWKS format (RS256 only) |
@@ -270,16 +413,6 @@ All responses use the envelope:
270
413
  { "error": false, "statusCode": 200, "message": "...", "data": { ... } }
271
414
  ```
272
415
 
273
- ### Change Identifier
274
-
275
- ```
276
- PATCH /me/identifier
277
- Authorization: Bearer <token>
278
- Content-Type: application/json
279
-
280
- { "identifier": "new@example.com" }
281
- ```
282
-
283
416
  ### Change Password
284
417
 
285
418
  ```
@@ -302,6 +435,7 @@ Verifies the JWT and sets `req.user`. In server mode, also performs silent token
302
435
 
303
436
  ```typescript
304
437
  app.get('/dashboard', auth.protect(), (req, res) => res.json(req.user));
438
+ // req.user: { id, identifier, identifierType, roles, identifiers? }
305
439
  ```
306
440
 
307
441
  Token is read from `Authorization: Bearer <token>` header or `access_token` cookie.
@@ -348,7 +482,7 @@ Available on `ServerAuthClient` only:
348
482
  const auth = createAuth({ mode: 'server', ... });
349
483
 
350
484
  // Sign
351
- const accessToken = auth.signAccessToken({ id, identifier, roles });
485
+ const accessToken = auth.signAccessToken({ id, identifier, identifierType, roles });
352
486
  const refreshToken = auth.signRefreshToken(sessionId);
353
487
 
354
488
  // Verify
@@ -376,8 +510,10 @@ app.use(auth.errorHandler()); // must be last
376
510
  | Code | HTTP | Meaning |
377
511
  |---|---|---|
378
512
  | `INVALID_CREDENTIALS` | 401 | Wrong identifier or password |
379
- | `USER_ALREADY_EXISTS` | 409 | Duplicate identifier at registration |
380
513
  | `USER_NOT_FOUND` | 404 | User does not exist |
514
+ | `USER_ALREADY_EXISTS` | 409 | Duplicate identifier at registration |
515
+ | `IDENTIFIER_NOT_FOUND` | 404 | Referenced identifier ID does not exist or belongs to another user |
516
+ | `IDENTIFIER_ALREADY_EXISTS` | 409 | Identifier value already taken by another user |
381
517
  | `TOKEN_EXPIRED` | 401 | JWT `exp` is in the past |
382
518
  | `TOKEN_INVALID` | 401 | Bad signature or malformed JWT |
383
519
  | `UNAUTHORIZED` | 401 | No valid token or session not found |
@@ -455,6 +591,18 @@ app.use(createIdempotencyMiddleware({ redisUrl: 'redis://localhost:6379' }));
455
591
 
456
592
  ## Migration Guide
457
593
 
594
+ ### 4.0.0 Breaking Changes
595
+
596
+ | What changed | Action required |
597
+ |---|---|
598
+ | `sentri_users` no longer has an `identifier` column | Drop and recreate tables — identities now live in `sentri_identifiers` |
599
+ | New `sentri_identifiers` table | Created automatically by `auth.migrate()` |
600
+ | `RegisterInput.identifier` → `RegisterInput.identifiers` (array) | Update register calls to pass `identifiers: [{ type, value }]` |
601
+ | `AuthUser` now includes `identifierType` | Update any code reading `req.user` to expect this new field |
602
+ | `PATCH /me/identifier` removed | Use `PUT /me/identifiers` for updates, `PATCH /me/identifiers/primary` for primary change |
603
+ | `changeIdentifier()` removed from `ServerAuthClient` | Use `bulkUpdateIdentifiers()` or `changePrimaryIdentifier()` |
604
+ | New error codes: `IDENTIFIER_NOT_FOUND`, `IDENTIFIER_ALREADY_EXISTS` | Handle these in your error handlers as needed |
605
+
458
606
  ### 3.0.0 Breaking Changes
459
607
 
460
608
  | What changed | Action required |
package/dist/index.d.ts CHANGED
@@ -7,6 +7,8 @@ import { Request, ErrorRequestHandler, RequestHandler, Router } from 'express';
7
7
  * - `INVALID_CREDENTIALS` — identifier or password did not match (intentionally vague to prevent user enumeration)
8
8
  * - `USER_NOT_FOUND` — an operation required a user that does not exist
9
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
10
12
  * - `TOKEN_EXPIRED` — the JWT was valid but its `exp` claim is in the past
11
13
  * - `TOKEN_INVALID` — the JWT could not be verified (bad signature, malformed, wrong type)
12
14
  * - `FORBIDDEN` — the user is authenticated but lacks the required role
@@ -18,7 +20,7 @@ import { Request, ErrorRequestHandler, RequestHandler, Router } from 'express';
18
20
  * When you extend {@link SentriError} for your own error types you can use any
19
21
  * string as `code` — it does not need to be one of these built-in values.
20
22
  */
21
- 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';
22
24
  /**
23
25
  * Default HTTP status codes for built-in error codes.
24
26
  * Custom codes not in this map default to 500.
@@ -97,10 +99,34 @@ interface ApiResponse<T = null> {
97
99
  message: string;
98
100
  data: T | null;
99
101
  }
102
+ /** A single identifier entry belonging to a user. */
103
+ interface IdentifierRecord {
104
+ id: string;
105
+ type: string;
106
+ value: string;
107
+ isPrimary: boolean;
108
+ }
109
+ /**
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.
113
+ */
100
114
  interface AuthUser<TRole extends string = string> {
101
115
  id: string;
116
+ /** Primary identifier value — embedded in the JWT payload. */
102
117
  identifier: string;
118
+ /** Type of the primary identifier (e.g. 'email', 'username'). */
119
+ identifierType: string;
103
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;
104
130
  }
105
131
  type RegisterResult<TRole extends string = string> = {
106
132
  success: true;
@@ -132,9 +158,16 @@ type GetUserResult<TRole extends string = string> = {
132
158
  success: false;
133
159
  error: SentriError;
134
160
  };
135
- type ChangeIdentifierResult<TRole extends string = string> = {
161
+ type BulkIdentifiersResult = {
136
162
  success: true;
137
- user: AuthUser<TRole>;
163
+ identifiers: IdentifierRecord[];
164
+ } | {
165
+ success: false;
166
+ error: SentriError;
167
+ };
168
+ type ChangePrimaryResult = {
169
+ success: true;
170
+ identifiers: IdentifierRecord[];
138
171
  } | {
139
172
  success: false;
140
173
  error: SentriError;
@@ -155,11 +188,17 @@ type RefreshResult<TRole extends string = string> = {
155
188
  error: SentriError;
156
189
  };
157
190
  interface RegisterInput<TRole extends string = string> {
158
- identifier: string;
191
+ /**
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.
195
+ */
196
+ identifiers: IdentifierInput[];
159
197
  password: string;
160
198
  roles?: TRole[];
161
199
  }
162
200
  interface LoginInput {
201
+ /** Any of the user's identifier values — Sentri searches all types. */
163
202
  identifier: string;
164
203
  password: string;
165
204
  }
@@ -188,7 +227,14 @@ interface RouterHandlers {
188
227
  logout?: (refreshToken: string | undefined) => Promise<void>;
189
228
  logoutAll?: (userId: string) => Promise<void>;
190
229
  assignRoles?: (userId: string, roles: string[]) => Promise<AssignRolesResult>;
191
- changeIdentifier?: (userId: string, newIdentifier: string) => Promise<ChangeIdentifierResult>;
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>;
192
238
  changePassword?: (userId: string, currentPassword: string, newPassword: string) => Promise<ChangePasswordResult>;
193
239
  }
194
240
  interface ServerAuthConfig<TRole extends string = string> {
@@ -406,15 +452,13 @@ interface ServerAuthClient<TRole extends string = string> extends AuthClient<TRo
406
452
  /** Extract the raw access token from an Express request. */
407
453
  getCurrentAccessToken(request: Request): string | undefined;
408
454
  /**
409
- * Pre-built Express Router with auth endpoints:
410
- * POST /register, POST /login, POST /refresh, POST /logout,
411
- * POST /logout-all, GET /me, POST /users/:userId/roles,
412
- * GET /keys (only when algorithm is RS256/RS384/RS512)
455
+ * Pre-built Express Router with auth endpoints.
456
+ * See README for the full endpoint list.
413
457
  */
414
458
  router(): Router;
415
459
  /**
416
- * Run database migrations to create sentri_users and sentri_sessions tables.
417
- * Safe to call on every startup — uses IF NOT EXISTS.
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.
418
462
  */
419
463
  migrate(): Promise<void>;
420
464
  /**
@@ -423,9 +467,9 @@ interface ServerAuthClient<TRole extends string = string> extends AuthClient<TRo
423
467
  * Uses Redis backend when `redisUrl` is set in server config; otherwise in-memory.
424
468
  */
425
469
  idempotencyMiddleware(options?: IdempotencyOptions): RequestHandler;
426
- /** Register a new user. */
470
+ /** Register a new user with one or more identifiers. */
427
471
  register(input: RegisterInput<TRole>): Promise<RegisterResult<TRole>>;
428
- /** Authenticate a user, returns access + refresh tokens. */
472
+ /** Authenticate a user by any of their identifier values, returns access + refresh tokens. */
429
473
  login(input: LoginInput): Promise<AuthResult<TRole>>;
430
474
  /** Rotate a refresh token, returns new token pair. */
431
475
  refresh(refreshToken: string): Promise<RefreshResult<TRole>>;
@@ -433,14 +477,24 @@ interface ServerAuthClient<TRole extends string = string> extends AuthClient<TRo
433
477
  logout(refreshToken: string): Promise<void>;
434
478
  /** Invalidate all sessions for a user. */
435
479
  logoutAll(userId: string): Promise<void>;
436
- /** Fetch a user by ID. Returns `success: false` if not found. */
480
+ /** Fetch a user by ID, including all their identifiers. Returns `success: false` if not found. */
437
481
  getUser(userId: string): Promise<GetUserResult<TRole>>;
438
- /** Change a user's identifier (username/email). */
439
- changeIdentifier(userId: string, newIdentifier: string): Promise<ChangeIdentifierResult<TRole>>;
440
482
  /** Change a user's password and revoke all their sessions. */
441
483
  changePassword(userId: string, currentPassword: string, newPassword: string): Promise<ChangePasswordResult>;
442
484
  /** Add roles to a user (existing roles are preserved). */
443
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>;
444
498
  }
445
499
  interface ClientAuthClient<TRole extends string = string> extends AuthClient<TRole> {
446
500
  }
@@ -549,4 +603,4 @@ declare global {
549
603
  }
550
604
  }
551
605
 
552
- export { type AccessCookieConfig, type ApiResponse, type AssignRolesResult, type AuthClient, type AuthConfig, type AuthHooks, type AuthResult, type AuthUser, type ChangeIdentifierResult, type ChangePasswordResult, type ClientAuthClient, type ClientAuthConfig, type CookieConfig, type CreateServerOptions, type ErrorHandlerOptions, type GetUserResult, type IdempotencyOptions, 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 };
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 };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import Re from'bcrypt';import H from'jsonwebtoken';import {generateKeyPairSync,randomUUID,createPublicKey,createPrivateKey}from'crypto';import {Kysely,sql,PostgresDialect}from'kysely';import {Router}from'express';import {Redis}from'ioredis';import {Pool}from'pg';var pe=Object.assign(Object.create(null),{UNAUTHORIZED:401,TOKEN_EXPIRED:401,TOKEN_INVALID:401,INVALID_CREDENTIALS:401,FORBIDDEN:403,USER_NOT_FOUND:404,USER_ALREADY_EXISTS:409,INVALID_ROLE:400,VALIDATION_ERROR:400,CONFIGURATION_ERROR:500}),i=class extends Error{code;statusCode;constructor(r,s,t){super(s),this.name="SentriError",this.code=r,this.statusCode=t??pe[r]??500;}};async function D(e,r=12){return Re.hash(e,r)}async function K(e,r){return Re.compare(e,r)}var ye=new Map,we=new Map,nr=3600*1e3;function Ce(e){let r=ye.get(e);if(!r){let s=createPrivateKey(e),t=createPublicKey(s),n=t.export({format:"jwk"}),o=Buffer.from(e).slice(0,8).toString("base64url"),c={...n,use:"sig",kid:o};r={kid:o,publicKey:t,jwk:c},ye.set(e,r);}return r}function ke(e){let{jwk:r}=Ce(e);return {keys:[r]}}function Ie(e){return Ce(e).publicKey}async function Se(e){let r=Date.now(),s=we.get(e);if(s&&r-s.fetchedAt<nr)return s.publicKey;let t=await fetch(e);if(!t.ok)throw new i("CONFIGURATION_ERROR",`Failed to fetch public key from ${e}: HTTP ${t.status}`);let n=await t.json();if(!n.keys||n.keys.length===0)throw new i("CONFIGURATION_ERROR",`No keys found in JWKS response from ${e}`);let o=n.keys[0],c=createPublicKey({key:o,format:"jwk"});return we.set(e,{publicKey:c,fetchedAt:r}),c}var Te=new WeakMap,ve=32,Ee=10,xe=31;function _e(e){if(e.mode==="client"){if(!e.keyUri||e.keyUri.trim().length===0)throw new i("CONFIGURATION_ERROR","keyUri must not be empty");return}if(!e.secret||e.secret.trim().length===0)throw new i("CONFIGURATION_ERROR","secret must not be empty");if((e.algorithm??"HS256").startsWith("HS")&&e.secret.length<ve)throw new i("CONFIGURATION_ERROR",`secret must be at least ${ve} characters for HMAC algorithms`);let t=e.saltRounds??12;if(!Number.isInteger(t)||t<Ee||t>xe)throw new i("CONFIGURATION_ERROR",`saltRounds must be an integer between ${Ee} and ${xe}`);if(!e.validRoles||e.validRoles.length===0)throw new i("CONFIGURATION_ERROR","validRoles must contain at least one role");if(!e.dialect)throw new i("CONFIGURATION_ERROR","dialect is required in server mode")}function p(e){let r=Te.get(e);if(r)return r;let s={algorithm:e.algorithm??"HS256",accessExpiresIn:e.accessExpiresIn??"15m",refreshExpiresIn:e.refreshExpiresIn??"7d",saltRounds:e.saltRounds??12,validRoles:e.validRoles,validRolesSet:new Set(e.validRoles),cookieName:e.cookie?.name??"refresh_token",accessCookieName:e.accessCookie?.name??"access_token"};return Te.set(e,s),s}var or=/^(\d+)([smhdw])$/,ir={s:1e3,m:6e4,h:36e5,d:864e5,w:6048e5},be=new Map;function x(e){if(typeof e=="number")return e*1e3;let r=be.get(e);if(r!==void 0)return r;let s=or.exec(e);if(!s?.[1]||!s?.[2])throw new Error(`Invalid expiresIn: "${e}". Use e.g. "15m", "7d", "1h".`);let t=ir[s[2]];if(t===void 0)throw new Error(`Invalid expiresIn: "${e}". Use e.g. "15m", "7d", "1h".`);let n=parseInt(s[1],10)*t;return be.set(e,n),n}var Pe=new Map,Oe=new Map,Ne=new Map;function Ue(e){return e.startsWith("RS")||e.startsWith("PS")}function De(e){let r=Pe.get(e);return r||(r={access:`${e}:access`,refresh:`${e}:refresh`},Pe.set(e,r)),r}function cr(e){let r=Oe.get(e);return r||(r=createPrivateKey(e),Oe.set(e,r)),r}function Ke(e){let r=p(e);if(Ue(r.algorithm)){let n=cr(e.secret);return {accessKey:n,refreshKey:n}}let{access:s,refresh:t}=De(e.secret);return {accessKey:s,refreshKey:t}}function He(e,r){let s=p(e);if(Ue(s.algorithm))return Ie(e.secret);let{access:t,refresh:n}=De(e.secret);return r==="access"?t:n}function je(e,r,s,t){let n=`${s}:${t}`,o=Ne.get(n);return o||(o={expiresIn:s,algorithm:t},Ne.set(n,o)),H.sign(e,r,o)}function Le(e,r,s){try{let t=H.verify(e,r,{algorithms:[s]});if(typeof t=="string"||t===null)throw new i("TOKEN_INVALID","Token payload is not an object");return t}catch(t){throw t instanceof i?t:t instanceof H.TokenExpiredError?new i("TOKEN_EXPIRED","Token has expired"):new i("TOKEN_INVALID","Token is invalid or malformed")}}function j(e,r){let s=p(r),{accessKey:t}=Ke(r);return je(e,t,s.accessExpiresIn,s.algorithm)}function L(e,r){let s=p(r),{refreshKey:t}=Ke(r);return je({sessionId:e},t,s.refreshExpiresIn,s.algorithm)}function z(e,r){let s=p(r),t=He(r,"access");return Le(e,t,s.algorithm)}function F(e,r){let s=p(r),t=He(r,"refresh");return Le(e,t,s.algorithm)}function Fe(e,r){try{let s=H.verify(e,r,{algorithms:["RS256","RS384","RS512","PS256","PS384","PS512"]});if(typeof s=="string"||s===null)throw new i("TOKEN_INVALID","Token payload is not an object");return s}catch(s){throw s instanceof i?s:s instanceof H.TokenExpiredError?new i("TOKEN_EXPIRED","Token has expired"):new i("TOKEN_INVALID","Token is invalid or malformed")}}function E(e){return p(e).cookieName}function b(e,r){if(!e)return;let s=`${r}=`,t=0;for(;t<e.length;){for(;t<e.length&&e[t]===" ";)t++;let n=e.indexOf(";",t),o=n===-1?e.length:n;if(e.startsWith(s,t))return e.slice(t+s.length,o);t=o+1;}}function q(e,r,s){let t=s.cookie??{},n=p(s),o=x(n.refreshExpiresIn);e.cookie(E(s),r,{httpOnly:t.httpOnly??true,secure:t.secure??false,sameSite:t.sameSite??"strict",path:t.path??"/",maxAge:o});}function W(e,r){let s=r.cookie??{};e.clearCookie(E(r),{path:s.path??"/"});}function ce(e){return p(e).accessCookieName}function M(e,r,s){if(!s.accessCookie)return;let t=s.accessCookie,n=p(s),o=x(n.accessExpiresIn);e.cookie(ce(s),r,{httpOnly:false,secure:t.secure??false,sameSite:t.sameSite??"strict",path:t.path??"/",maxAge:o});}function ue(e,r){if(!r.accessCookie)return;let s=r.accessCookie;e.clearCookie(ce(r),{path:s.path??"/"});}function $(e,r){let s=e.headers.authorization;return s?.startsWith("Bearer ")?s.slice(7):b(e.headers.cookie,ce(r))}var qe=new Map;function k(e){let r=qe.get(e);return r||(r=new Kysely({dialect:e}),qe.set(e,r)),r}function de(e){try{return JSON.parse(e)}catch{return []}}function $e(e){return JSON.stringify(e)}async function Z(e,r){let s=await e.selectFrom("sentri_users").selectAll().where("identifier","=",r).executeTakeFirst();return s?{id:s.id,identifier:s.identifier,passwordHash:s.password_hash,roles:de(s.roles)}:null}async function J(e,r){let s=await e.selectFrom("sentri_users").selectAll().where("id","=",r).executeTakeFirst();return s?{id:s.id,identifier:s.identifier,passwordHash:s.password_hash,roles:de(s.roles)}:null}async function Je(e,r){let s=randomUUID();return await e.insertInto("sentri_users").values({id:s,identifier:r.identifier,password_hash:r.passwordHash,roles:$e(r.roles)}).execute(),{id:s}}async function Ge(e,r,s){await e.updateTable("sentri_users").set({identifier:s}).where("id","=",r).execute();}async function Xe(e,r,s){await e.updateTable("sentri_users").set({password_hash:s}).where("id","=",r).execute();}async function Ve(e,r,s){await e.updateTable("sentri_users").set({roles:$e(s)}).where("id","=",r).execute();}async function le(e,r){let s=randomUUID();return await e.insertInto("sentri_sessions").values({id:s,user_id:r.userId,expires_at:r.expiresAt}).execute(),{id:s}}async function Be(e,r){let s=await e.selectFrom("sentri_sessions as s").innerJoin("sentri_users as u","u.id","s.user_id").select(["s.id as session_id","s.user_id","s.expires_at","s.created_at as session_created_at","u.id as user_id_col","u.identifier","u.password_hash","u.roles"]).where("s.id","=",r).executeTakeFirst();return s?{id:s.session_id,userId:s.user_id,expiresAt:new Date(s.expires_at),createdAt:new Date(s.session_created_at),user:{id:s.user_id_col,identifier:s.identifier,passwordHash:s.password_hash,roles:de(s.roles)}}:null}async function Y(e,r){await e.deleteFrom("sentri_sessions").where("id","=",r).execute();}async function fe(e,r){await e.deleteFrom("sentri_sessions").where("user_id","=",r).execute();}async function G(e,r){let s=p(r),t=k(r.dialect),n=e.roles??[],o=n.filter(w=>!s.validRolesSet.has(w));if(o.length>0)return {success:false,error:new i("INVALID_ROLE",`Invalid roles: ${o.join(", ")}`)};let c=e.identifier.trim();if(await Z(t,c))return {success:false,error:new i("USER_ALREADY_EXISTS","User already exists")};let g=await D(e.password,s.saltRounds);return {success:true,user:{id:(await Je(t,{identifier:c,passwordHash:g,roles:n})).id,identifier:c,roles:n}}}async function Q(e,r){let s=p(r),t=k(r.dialect),n=await Z(t,e.identifier.trim());if(!n)return {success:false,error:new i("INVALID_CREDENTIALS","Invalid credentials")};if(!await K(e.password,n.passwordHash))return {success:false,error:new i("INVALID_CREDENTIALS","Invalid credentials")};let c=new Date(Date.now()+x(s.refreshExpiresIn)),d=await le(t,{userId:n.id,expiresAt:c}),g={id:n.id,identifier:n.identifier,roles:n.roles},m=j({id:n.id,identifier:n.identifier,roles:n.roles,sessionId:d.id},r),w=L(d.id,r);return {success:true,accessToken:m,refreshToken:w,user:g}}async function _(e,r){let s=p(r),t=k(r.dialect),n;try{({sessionId:n}=F(e,r));}catch(v){return v instanceof i?{success:false,error:v}:{success:false,error:new i("TOKEN_INVALID","Invalid refresh token")}}let o=await Be(t,n);if(!o)return {success:false,error:new i("UNAUTHORIZED","Session not found or revoked")};if(o.expiresAt.getTime()<Date.now())return await Y(t,n),{success:false,error:new i("TOKEN_EXPIRED","Session has expired")};await Y(t,n);let c=new Date(Date.now()+x(s.refreshExpiresIn)),d=await le(t,{userId:o.userId,expiresAt:c}),g={id:o.user.id,identifier:o.user.identifier,roles:o.user.roles},m=j({id:g.id,identifier:g.identifier,roles:g.roles,sessionId:d.id},r),w=L(d.id,r);return {success:true,accessToken:m,refreshToken:w,user:g}}async function ee(e,r){let s=k(r.dialect),t;try{({sessionId:t}=F(e,r));}catch{return}await Y(s,t);}async function re(e,r){let s=k(r.dialect);await fe(s,e);}async function ze(e,r){let s=k(r.dialect),t=await J(s,e);return t?{success:true,user:{id:t.id,identifier:t.identifier,roles:t.roles}}:{success:false,error:new i("USER_NOT_FOUND","User not found")}}async function te(e,r,s){let t=k(s.dialect),n=await J(t,e);if(!n)return {success:false,error:new i("USER_NOT_FOUND","User not found")};let o=r.trim();return await Z(t,o)?{success:false,error:new i("USER_ALREADY_EXISTS","Identifier already taken")}:(await Ge(t,e,o),{success:true,user:{id:n.id,identifier:o,roles:n.roles}})}async function se(e,r,s,t){let n=p(t),o=k(t.dialect),c=await J(o,e);if(!c)return {success:false,error:new i("USER_NOT_FOUND","User not found")};if(!await K(r,c.passwordHash))return {success:false,error:new i("INVALID_CREDENTIALS","Invalid credentials")};let g=await D(s,n.saltRounds);return await Xe(o,e,g),await fe(o,e),{success:true}}async function ne(e,r,s){let t=p(s),n=k(s.dialect),o=r.filter(m=>!t.validRolesSet.has(m));if(o.length>0)return {success:false,error:new i("INVALID_ROLE",`Invalid roles: ${o.join(", ")}`)};let c=await J(n,e);if(!c)return {success:false,error:new i("USER_NOT_FOUND","User not found")};let d=new Set(c.roles);for(let m of r)d.add(m);let g=Array.from(d);return await Ve(n,e,g),{success:true,user:{id:c.id,identifier:c.identifier,roles:g}}}function S(e){return e.mode==="client"?dr(e.keyUri):lr(e)}function dr(e){return async(r,s,t)=>{let n=r.headers.authorization,o=n?.startsWith("Bearer ")?n.slice(7):void 0;if(!o)return t(new i("UNAUTHORIZED","Missing or malformed Authorization header"));try{let c=await Se(e),d=Fe(o,c);r.user={id:d.id,identifier:d.identifier,roles:d.roles},t();}catch(c){t(c);}}}function lr(e){return async(r,s,t)=>{let n=$(r,e);if(!n)return t(new i("UNAUTHORIZED","Missing or malformed Authorization header"));try{let o=z(n,e);if(e.isTokenRevoked&&await e.isTokenRevoked(o.sessionId))return t(new i("UNAUTHORIZED","Token has been revoked"));r.user={id:o.id,identifier:o.identifier,roles:o.roles},t();}catch(o){if(o instanceof i&&o.code==="TOKEN_EXPIRED"){let c=b(r.headers.cookie,E(e));if(!c)return t(new i("UNAUTHORIZED","Token expired. Please login again."));try{let d=await _(c,e);if(!d.success)return t(new i("UNAUTHORIZED","Session expired. Please login again."));q(s,d.refreshToken,e),M(s,d.accessToken,e),s.setHeader("X-New-Access-Token",d.accessToken),r.user=d.user,t();}catch{t(new i("UNAUTHORIZED","Session expired. Please login again."));}}else t(o);}}}function X(...e){let r=`Requires one of roles: ${e.join(", ")}`;return (s,t,n)=>{if(!s.user)return n(new i("UNAUTHORIZED","Not authenticated"));let o=s.user.roles;if(!e.some(c=>o.includes(c)))return n(new i("FORBIDDEN",r));n();}}var fr=new i("FORBIDDEN","You do not have permission to perform this action");function V(e){let r=typeof e=="function"?{check:e}:e;return async(s,t,n)=>{if(!s.user)return n(new i("UNAUTHORIZED","Not authenticated"));if(r.roles&&r.roles.length>0){let o=s.user.roles;if(r.roles.some(c=>o.includes(c)))return n()}try{let o=r.check(s);(o instanceof Promise?await o:o)?n():n(fr);}catch(o){n(o);}}}function P(e){return (r,s,t,n)=>{if(r instanceof i){t.status(r.statusCode).json({error:true,statusCode:r.statusCode,code:r.code,message:r.message,data:null});return}e?.onUnhandled?.(r),t.status(500).json({error:true,statusCode:500,code:"INTERNAL_SERVER_ERROR",message:"Internal server error",data:null});}}var oe=8,O=72,N=255;function y(e){return new i("VALIDATION_ERROR",e)}function T(e,r,s,t){e.status(r).json({error:false,statusCode:r,message:s,data:t});}function U(e,r){e.status(r.statusCode).json({error:true,statusCode:r.statusCode,code:r.code,message:r.message,data:null});}function B(e){if(e==null||typeof e!="object"||Array.isArray(e))throw new i("VALIDATION_ERROR","Request body is missing or not a JSON object. Did you apply express.json()?");return e}function hr(e,r){if(!r.apiKey)return;let s=e.headers["x-api-key"];if(typeof s!="string"||s!==r.apiKey)throw new i("UNAUTHORIZED","Invalid or missing API key")}function ge(e){if(e)try{let r=e();r instanceof Promise&&r.catch(()=>{});}catch{}}function mr(e){return e.startsWith("RS")||e.startsWith("PS")}function We(e){let r=Router(),s=e,t=p(s),n=e.router?.register??(a=>G(a,s)),o=e.router?.login??(a=>Q(a,s)),c=e.router?.refresh??(a=>_(a,s)),d=e.router?.logout??(a=>a!==void 0?ee(a,s):Promise.resolve()),g=e.router?.logoutAll??(a=>re(a,s)),m=e.router?.assignRoles??((a,u)=>ne(a,u,s)),w=e.router?.changeIdentifier??((a,u)=>te(a,u,s)),v=e.router?.changePassword??((a,u,R)=>se(a,u,R,s));mr(t.algorithm)&&r.get("/keys",(a,u)=>{u.setHeader("Cache-Control","public, max-age=3600"),u.json(ke(e.secret));}),r.post("/register",async(a,u,R)=>{try{hr(a,e);let l=B(a.body),{identifier:f,password:h,roles:A}=l;if(typeof f!="string"||f.trim().length===0)throw y("identifier is required and must be a non-empty string");if(f.length>N)throw y(`identifier must not exceed ${N} characters`);if(typeof h!="string"||h.length<oe)throw y(`password is required and must be at least ${oe} characters`);if(h.length>O)throw y(`password must not exceed ${O} characters`);if(A!==void 0&&!Array.isArray(A))throw y("roles must be an array of strings when provided");if(Array.isArray(A)&&!A.every(tr=>typeof tr=="string"))throw y("each role must be a string");let C=Array.isArray(A)?A:void 0,ie=C!==void 0?{identifier:f.trim(),password:h,roles:C}:{identifier:f.trim(),password:h},ae=await n(ie);if(!ae.success){U(u,ae.error);return}T(u,201,"User registered successfully",{user:ae.user});}catch(l){R(l);}}),r.post("/login",async(a,u,R)=>{try{let l=B(a.body),{identifier:f,password:h}=l;if(typeof f!="string"||f.trim().length===0)throw y("identifier is required and must be a non-empty string");if(f.length>N)throw y(`identifier must not exceed ${N} characters`);if(typeof h!="string"||h.length===0)throw y("password is required");if(h.length>O)throw y(`password must not exceed ${O} characters`);let A=f.trim(),C=await o({identifier:A,password:h});if(!C.success){ge(()=>e.hooks?.onFailedLogin?.(A,C.error)),U(u,C.error);return}ge(()=>e.hooks?.onLogin?.(C.user)),q(u,C.refreshToken,e),M(u,C.accessToken,e),T(u,200,"Login successful",{accessToken:C.accessToken,user:C.user});}catch(l){R(l);}}),r.post("/refresh",async(a,u,R)=>{try{let l=b(a.headers.cookie,E(e));if(!l)throw new i("UNAUTHORIZED","Refresh token cookie is missing");let f=await c(l);if(!f.success){W(u,e),U(u,f.error);return}q(u,f.refreshToken,e),M(u,f.accessToken,e),T(u,200,"Token refreshed",{accessToken:f.accessToken});}catch(l){R(l);}}),r.post("/logout",async(a,u,R)=>{try{let l=b(a.headers.cookie,E(e));await d(l),W(u,e),ue(u,e),T(u,200,"Logged out",null);}catch(l){R(l);}}),r.post("/logout-all",S(e),async(a,u,R)=>{try{let l=a.user.id;await g(l),ge(()=>e.hooks?.onLogout?.(l)),W(u,e),ue(u,e),T(u,200,"All sessions revoked",null);}catch(l){R(l);}}),r.get("/me",S(e),(a,u)=>{T(u,200,"OK",a.user);});let I=V(a=>!!a.user);return r.patch("/me/identifier",S(e),I,async(a,u,R)=>{try{let l=B(a.body),{newIdentifier:f}=l;if(typeof f!="string"||f.trim().length===0)throw y("newIdentifier is required and must be a non-empty string");if(f.length>N)throw y(`newIdentifier must not exceed ${N} characters`);let h=await w(a.user.id,f);if(!h.success){U(u,h.error);return}T(u,200,"Identifier updated successfully",{user:h.user});}catch(l){R(l);}}),r.patch("/me/password",S(e),I,async(a,u,R)=>{try{let l=B(a.body),{currentPassword:f,newPassword:h}=l;if(typeof f!="string"||f.length===0)throw y("currentPassword is required");if(typeof h!="string"||h.length<oe)throw y(`newPassword must be at least ${oe} characters`);if(h.length>O)throw y(`newPassword must not exceed ${O} characters`);if(f===h)throw y("newPassword must be different from currentPassword");let A=await v(a.user.id,f,h);if(!A.success){U(u,A.error);return}T(u,200,"Password updated successfully. All sessions have been revoked.",null);}catch(l){R(l);}}),r.post("/users/:userId/roles",S(e),X("admin"),async(a,u,R)=>{try{let l=B(a.body),{roles:f}=l,h=a.params.userId,A=typeof h=="string"?h:void 0;if(!A)throw y("userId is required");if(!Array.isArray(f)||f.length===0)throw y("roles must be a non-empty array of strings");if(!f.every(ie=>typeof ie=="string"))throw y("each role must be a string");let C=await m(A,f);if(!C.success){U(u,C.error);return}T(u,200,"Roles assigned successfully",{user:C.user});}catch(l){R(l);}}),r.use(P()),r}var Ze=new Map;function Ye(e){let r=Ze.get(e);return r||(r=new Redis(e,{lazyConnect:false,enableOfflineQueue:false}),Ze.set(e,r)),r}function he(e){let r=e?.ttl??3e5,s=(e?.header??"X-Idempotency-Key").toLowerCase(),t=new Set((e?.methods??["POST","PUT","PATCH"]).map(o=>o.toUpperCase())),n=e?.redisUrl;return n?Rr(n,r,s,t):yr(r,s,t,e?.maxSize??1e4)}function Rr(e,r,s,t){let n=Ye(e),o="sentri:idempotency:";return async(c,d,g)=>{let m=c.headers[s];if(!m||typeof m!="string"||!t.has(c.method))return g();c.requestId=m,d.setHeader("X-Request-Id",m);let w=await n.get(`${o}${m}`);if(w){let I=JSON.parse(w);return d.setHeader("X-Idempotent-Replayed","true"),d.status(I.statusCode).json(I.body)}let v=d.json.bind(d);d.json=function(a){if(d.statusCode>=200&&d.statusCode<300){let u={statusCode:d.statusCode,body:a,expiresAt:Date.now()+r};n.set(`${o}${m}`,JSON.stringify(u),"PX",r).catch(()=>{});}return v(a)},g();}}function yr(e,r,s,t){let n=Math.max(e,5e3),o=new Map,c=setInterval(()=>{let d=Date.now();for(let[g,m]of o)m.expiresAt<=d&&o.delete(g);},n);return typeof c=="object"&&c!==null&&"unref"in c&&c.unref(),(d,g,m)=>{let w=d.headers[r];if(!w||typeof w!="string"||!s.has(d.method))return m();d.requestId=w,g.setHeader("X-Request-Id",w);let v=Date.now(),I=o.get(w);if(I&&I.expiresAt>v)return g.setHeader("X-Idempotent-Replayed","true"),g.status(I.statusCode).json(I.body);let a=g.json.bind(g);g.json=function(R){if(g.statusCode>=200&&g.statusCode<300){if(o.size>=t){let l=o.keys().next().value;l!==void 0&&o.delete(l);}o.set(w,{statusCode:g.statusCode,body:R,expiresAt:Date.now()+e});}return a(R)},m();}}async function er(e){await e.schema.createTable("sentri_users").ifNotExists().addColumn("id","varchar(36)",r=>r.primaryKey().notNull()).addColumn("identifier","varchar(255)",r=>r.notNull().unique()).addColumn("password_hash","varchar(255)",r=>r.notNull()).addColumn("roles","text",r=>r.notNull().defaultTo("[]")).addColumn("created_at","timestamp",r=>r.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)).execute(),await e.schema.createTable("sentri_sessions").ifNotExists().addColumn("id","varchar(36)",r=>r.primaryKey().notNull()).addColumn("user_id","varchar(36)",r=>r.notNull().references("sentri_users.id").onDelete("cascade")).addColumn("expires_at","timestamp",r=>r.notNull()).addColumn("created_at","timestamp",r=>r.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)).execute();}function me(e){if(_e(e),e.mode==="client")return {protect:()=>S(e),authorize:(...t)=>X(...t),permit:t=>V(t),errorHandler:t=>P(t)};let r=e,s=p(r);return {protect:()=>S(r),authorize:(...t)=>X(...t),permit:t=>V(t),hashPassword:t=>D(t,s.saltRounds),verifyPassword:(t,n)=>K(t,n),signAccessToken:t=>j(t,r),signRefreshToken:t=>L(t,r),verifyAccessToken:t=>z(t,r),verifyRefreshToken:t=>F(t,r),getCurrentAccessToken:t=>$(t,r),router:()=>We(r),migrate:()=>er(k(r.dialect)),errorHandler:t=>P(t),idempotencyMiddleware:t=>he({...t,...r.redisUrl!==void 0&&{redisUrl:r.redisUrl}}),register:t=>G(t,r),login:t=>Q(t,r),refresh:t=>_(t,r),logout:t=>ee(t,r),logoutAll:t=>re(t,r),getUser:t=>ze(t,r),changeIdentifier:(t,n)=>te(t,n,r),changePassword:(t,n,o)=>se(t,n,o,r),assignRoles:(t,n)=>ne(t,n,r)}}function rr(e){return new PostgresDialect({pool:new Pool(e)})}function kr(e){let{privateKey:r}=generateKeyPairSync("rsa",{modulusLength:2048,privateKeyEncoding:{type:"pkcs8",format:"pem"},publicKeyEncoding:{type:"spki",format:"pem"}}),s={mode:"server",dialect:rr(e.db),secret:r,algorithm:"RS256",validRoles:e.validRoles,...e.accessExpiresIn!==void 0&&{accessExpiresIn:e.accessExpiresIn},...e.refreshExpiresIn!==void 0&&{refreshExpiresIn:e.refreshExpiresIn},...e.saltRounds!==void 0&&{saltRounds:e.saltRounds},...e.apiKey!==void 0&&{apiKey:e.apiKey},...e.cookie!==void 0&&{cookie:e.cookie},...e.accessCookie!==void 0&&{accessCookie:e.accessCookie},...e.hooks!==void 0&&{hooks:e.hooks},...e.router!==void 0&&{router:e.router},...e.isTokenRevoked!==void 0&&{isTokenRevoked:e.isTokenRevoked},...e.redisUrl!==void 0&&{redisUrl:e.redisUrl}};return me(s)}export{pe as SENTRI_ERROR_STATUS,i as SentriError,me as createAuth,kr as createAuthServer,P as createErrorHandler,he as createIdempotencyMiddleware,$ as getCurrentAccessToken,G as register};
1
+ import xe from'bcrypt';import B from'jsonwebtoken';import {randomUUID,generateKeyPairSync,createPublicKey,createPrivateKey}from'crypto';import {Kysely,sql,PostgresDialect}from'kysely';import {Router}from'express';import {Redis}from'ioredis';import {Pool}from'pg';var be=Object.assign(Object.create(null),{UNAUTHORIZED:401,TOKEN_EXPIRED:401,TOKEN_INVALID:401,INVALID_CREDENTIALS:401,FORBIDDEN:403,USER_NOT_FOUND:404,IDENTIFIER_NOT_FOUND:404,USER_ALREADY_EXISTS:409,IDENTIFIER_ALREADY_EXISTS:409,INVALID_ROLE:400,VALIDATION_ERROR:400,CONFIGURATION_ERROR:500}),o=class extends Error{code;statusCode;constructor(r,s,t){super(s),this.name="SentriError",this.code=r,this.statusCode=t??be[r]??500;}};async function M(e,r=12){return xe.hash(e,r)}async function V(e,r){return xe.compare(e,r)}var Pe=new Map,Ne=new Map,Ar=3600*1e3;function Oe(e){let r=Pe.get(e);if(!r){let s=createPrivateKey(e),t=createPublicKey(s),i=t.export({format:"jwk"}),n=Buffer.from(e).slice(0,8).toString("base64url"),d={...i,use:"sig",kid:n};r={kid:n,publicKey:t,jwk:d},Pe.set(e,r);}return r}function Ue(e){let{jwk:r}=Oe(e);return {keys:[r]}}function Ke(e){return Oe(e).publicKey}async function He(e){let r=Date.now(),s=Ne.get(e);if(s&&r-s.fetchedAt<Ar)return s.publicKey;let t=await fetch(e);if(!t.ok)throw new o("CONFIGURATION_ERROR",`Failed to fetch public key from ${e}: HTTP ${t.status}`);let i=await t.json();if(!i.keys||i.keys.length===0)throw new o("CONFIGURATION_ERROR",`No keys found in JWKS response from ${e}`);let n=i.keys[0],d=createPublicKey({key:n,format:"jwk"});return Ne.set(e,{publicKey:d,fetchedAt:r}),d}var Fe=new WeakMap,je=32,Le=10,qe=31;function Me(e){if(e.mode==="client"){if(!e.keyUri||e.keyUri.trim().length===0)throw new o("CONFIGURATION_ERROR","keyUri must not be empty");return}if(!e.secret||e.secret.trim().length===0)throw new o("CONFIGURATION_ERROR","secret must not be empty");if((e.algorithm??"HS256").startsWith("HS")&&e.secret.length<je)throw new o("CONFIGURATION_ERROR",`secret must be at least ${je} characters for HMAC algorithms`);let t=e.saltRounds??12;if(!Number.isInteger(t)||t<Le||t>qe)throw new o("CONFIGURATION_ERROR",`saltRounds must be an integer between ${Le} and ${qe}`);if(!e.validRoles||e.validRoles.length===0)throw new o("CONFIGURATION_ERROR","validRoles must contain at least one role");if(!e.dialect)throw new o("CONFIGURATION_ERROR","dialect is required in server mode")}function A(e){let r=Fe.get(e);if(r)return r;let s={algorithm:e.algorithm??"HS256",accessExpiresIn:e.accessExpiresIn??"15m",refreshExpiresIn:e.refreshExpiresIn??"7d",saltRounds:e.saltRounds??12,validRoles:e.validRoles,validRolesSet:new Set(e.validRoles),cookieName:e.cookie?.name??"refresh_token",accessCookieName:e.accessCookie?.name??"access_token"};return Fe.set(e,s),s}var vr=/^(\d+)([smhdw])$/,Tr={s:1e3,m:6e4,h:36e5,d:864e5,w:6048e5},$e=new Map;function O(e){if(typeof e=="number")return e*1e3;let r=$e.get(e);if(r!==void 0)return r;let s=vr.exec(e);if(!s?.[1]||!s?.[2])throw new Error(`Invalid expiresIn: "${e}". Use e.g. "15m", "7d", "1h".`);let t=Tr[s[2]];if(t===void 0)throw new Error(`Invalid expiresIn: "${e}". Use e.g. "15m", "7d", "1h".`);let i=parseInt(s[1],10)*t;return $e.set(e,i),i}var Ve=new Map,Be=new Map,Je=new Map;function Xe(e){return e.startsWith("RS")||e.startsWith("PS")}function Ge(e){let r=Ve.get(e);return r||(r={access:`${e}:access`,refresh:`${e}:refresh`},Ve.set(e,r)),r}function Cr(e){let r=Be.get(e);return r||(r=createPrivateKey(e),Be.set(e,r)),r}function ze(e){let r=A(e);if(Xe(r.algorithm)){let i=Cr(e.secret);return {accessKey:i,refreshKey:i}}let{access:s,refresh:t}=Ge(e.secret);return {accessKey:s,refreshKey:t}}function We(e,r){let s=A(e);if(Xe(s.algorithm))return Ke(e.secret);let{access:t,refresh:i}=Ge(e.secret);return r==="access"?t:i}function Ze(e,r,s,t){let i=`${s}:${t}`,n=Je.get(i);return n||(n={expiresIn:s,algorithm:t},Je.set(i,n)),B.sign(e,r,n)}function Ye(e,r,s){try{let t=B.verify(e,r,{algorithms:[s]});if(typeof t=="string"||t===null)throw new o("TOKEN_INVALID","Token payload is not an object");return t}catch(t){throw t instanceof o?t:t instanceof B.TokenExpiredError?new o("TOKEN_EXPIRED","Token has expired"):new o("TOKEN_INVALID","Token is invalid or malformed")}}function J(e,r){let s=A(r),{accessKey:t}=ze(r);return Ze(e,t,s.accessExpiresIn,s.algorithm)}function X(e,r){let s=A(r),{refreshKey:t}=ze(r);return Ze({sessionId:e},t,s.refreshExpiresIn,s.algorithm)}function se(e,r){let s=A(r),t=We(r,"access");return Ye(e,t,s.algorithm)}function G(e,r){let s=A(r),t=We(r,"refresh");return Ye(e,t,s.algorithm)}function Qe(e,r){try{let s=B.verify(e,r,{algorithms:["RS256","RS384","RS512","PS256","PS384","PS512"]});if(typeof s=="string"||s===null)throw new o("TOKEN_INVALID","Token payload is not an object");return s}catch(s){throw s instanceof o?s:s instanceof B.TokenExpiredError?new o("TOKEN_EXPIRED","Token has expired"):new o("TOKEN_INVALID","Token is invalid or malformed")}}function D(e){return A(e).cookieName}function U(e,r){if(!e)return;let s=`${r}=`,t=0;for(;t<e.length;){for(;t<e.length&&e[t]===" ";)t++;let i=e.indexOf(";",t),n=i===-1?e.length:i;if(e.startsWith(s,t))return e.slice(t+s.length,n);t=n+1;}}function z(e,r,s){let t=s.cookie??{},i=A(s),n=O(i.refreshExpiresIn);e.cookie(D(s),r,{httpOnly:t.httpOnly??true,secure:t.secure??false,sameSite:t.sameSite??"strict",path:t.path??"/",maxAge:n});}function ie(e,r){let s=r.cookie??{};e.clearCookie(D(r),{path:s.path??"/"});}function we(e){return A(e).accessCookieName}function W(e,r,s){if(!s.accessCookie)return;let t=s.accessCookie,i=A(s),n=O(i.accessExpiresIn);e.cookie(we(s),r,{httpOnly:false,secure:t.secure??false,sameSite:t.sameSite??"strict",path:t.path??"/",maxAge:n});}function Ie(e,r){if(!r.accessCookie)return;let s=r.accessCookie;e.clearCookie(we(r),{path:s.path??"/"});}function Z(e,r){let s=e.headers.authorization;return s?.startsWith("Bearer ")?s.slice(7):U(e.headers.cookie,we(r))}var er=new Map;function T(e){let r=er.get(e);return r||(r=new Kysely({dialect:e}),er.set(e,r)),r}function Ae(e){try{return JSON.parse(e)}catch{return []}}function Sr(e){return JSON.stringify(e)}function tr(e){return {id:e.id,userId:e.user_id,type:e.type,value:e.value,isPrimary:e.is_primary===1,createdAt:new Date(e.created_at)}}async function Y(e,r){let s=await e.selectFrom("sentri_identifiers as i_login").innerJoin("sentri_users as u","u.id","i_login.user_id").innerJoin("sentri_identifiers as i_primary",t=>t.onRef("i_primary.user_id","=","u.id").on("i_primary.is_primary","=",1)).select(["u.id","u.password_hash","u.roles","i_primary.value as primary_value","i_primary.type as primary_type"]).where("i_login.value","=",r).executeTakeFirst();return s?{id:s.id,identifier:s.primary_value,identifierType:s.primary_type,passwordHash:s.password_hash,roles:Ae(s.roles)}:null}async function ne(e,r){let s=await e.selectFrom("sentri_users as u").innerJoin("sentri_identifiers as i",t=>t.onRef("i.user_id","=","u.id").on("i.is_primary","=",1)).select(["u.id","u.password_hash","u.roles","i.value as primary_value","i.type as primary_type"]).where("u.id","=",r).executeTakeFirst();return s?{id:s.id,identifier:s.primary_value,identifierType:s.primary_type,passwordHash:s.password_hash,roles:Ae(s.roles)}:null}async function sr(e,r,s){let t=s.map(i=>({id:randomUUID(),user_id:r,type:i.type,value:i.value,is_primary:i.isPrimary?1:0}));return await e.insertInto("sentri_identifiers").values(t).execute(),t.map(i=>({id:i.id,userId:i.user_id,type:i.type,value:i.value,isPrimary:i.is_primary===1,createdAt:new Date}))}async function K(e,r){return (await e.selectFrom("sentri_identifiers").selectAll().where("user_id","=",r).orderBy("created_at","asc").execute()).map(tr)}async function oe(e,r,s){let t=await e.selectFrom("sentri_identifiers").selectAll().where("id","=",r).where("user_id","=",s).executeTakeFirst();return t?tr(t):null}async function ir(e,r){let s=await e.selectFrom("sentri_identifiers").select(t=>t.fn.countAll().as("count")).where("user_id","=",r).executeTakeFirst();return Number(s?.count??0)}async function nr(e,r,s,t){await e.updateTable("sentri_identifiers").set({type:t.type,value:t.value}).where("id","=",r).where("user_id","=",s).execute();}async function or(e,r,s){await e.deleteFrom("sentri_identifiers").where("user_id","=",r).where("id","in",s).execute();}async function ar(e,r,s){await e.transaction().execute(async t=>{await t.updateTable("sentri_identifiers").set({is_primary:0}).where("user_id","=",r).execute(),await t.updateTable("sentri_identifiers").set({is_primary:1}).where("id","=",s).where("user_id","=",r).execute();});}async function ur(e,r,s){await e.updateTable("sentri_users").set({password_hash:s}).where("id","=",r).execute();}async function dr(e,r,s){await e.updateTable("sentri_users").set({roles:Sr(s)}).where("id","=",r).execute();}async function ve(e,r){let s=randomUUID();return await e.insertInto("sentri_sessions").values({id:s,user_id:r.userId,expires_at:r.expiresAt}).execute(),{id:s}}async function cr(e,r){let s=await e.selectFrom("sentri_sessions as s").innerJoin("sentri_users as u","u.id","s.user_id").innerJoin("sentri_identifiers as i",t=>t.onRef("i.user_id","=","u.id").on("i.is_primary","=",1)).select(["s.id as session_id","s.user_id","s.expires_at","s.created_at as session_created_at","u.id as user_id_col","u.password_hash","u.roles","i.value as primary_identifier","i.type as primary_identifier_type"]).where("s.id","=",r).executeTakeFirst();return s?{id:s.session_id,userId:s.user_id,expiresAt:new Date(s.expires_at),createdAt:new Date(s.session_created_at),user:{id:s.user_id_col,identifier:s.primary_identifier,identifierType:s.primary_identifier_type,passwordHash:s.password_hash,roles:Ae(s.roles)}}:null}async function ae(e,r){await e.deleteFrom("sentri_sessions").where("id","=",r).execute();}async function Te(e,r){await e.deleteFrom("sentri_sessions").where("user_id","=",r).execute();}async function Q(e,r){let s=A(r),t=T(r.dialect),i=e.roles??[],n=i.filter(R=>!s.validRolesSet.has(R));if(n.length>0)return {success:false,error:new o("INVALID_ROLE",`Invalid roles: ${n.join(", ")}`)};if(!e.identifiers||e.identifiers.length===0)return {success:false,error:new o("VALIDATION_ERROR","At least one identifier is required")};let d=e.identifiers.map(R=>({type:R.type.trim(),value:R.value.trim()}));if(new Set(d.map(R=>R.value)).size!==d.length)return {success:false,error:new o("VALIDATION_ERROR","Duplicate identifier values in request")};for(let R of d)if(await Y(t,R.value))return {success:false,error:new o("IDENTIFIER_ALREADY_EXISTS",`Identifier already taken: ${R.value}`)};let l=await M(e.password,s.saltRounds),{userId:g,identifierRows:v}=await t.transaction().execute(async R=>{let x=randomUUID();await R.insertInto("sentri_users").values({id:x,password_hash:l,roles:JSON.stringify(i)}).execute();let E=d.map((u,c)=>({id:randomUUID(),user_id:x,type:u.type,value:u.value,is_primary:c===0?1:0}));return await R.insertInto("sentri_identifiers").values(E).execute(),{userId:x,identifierRows:E}}),S=v.map(R=>({id:R.id,type:R.type,value:R.value,isPrimary:R.is_primary===1})),k=S[0];return {success:true,user:{id:g,identifier:k.value,identifierType:k.type,roles:i,identifiers:S}}}async function ue(e,r){let s=A(r),t=T(r.dialect),i=await Y(t,e.identifier.trim());if(!i)return {success:false,error:new o("INVALID_CREDENTIALS","Invalid credentials")};if(!await V(e.password,i.passwordHash))return {success:false,error:new o("INVALID_CREDENTIALS","Invalid credentials")};let d=new Date(Date.now()+O(s.refreshExpiresIn)),a=await ve(t,{userId:i.id,expiresAt:d}),l={id:i.id,identifier:i.identifier,identifierType:i.identifierType,roles:i.roles},g=J({id:i.id,identifier:i.identifier,identifierType:i.identifierType,roles:i.roles,sessionId:a.id},r),v=X(a.id,r);return {success:true,accessToken:g,refreshToken:v,user:l}}async function H(e,r){let s=A(r),t=T(r.dialect),i;try{({sessionId:i}=G(e,r));}catch(S){return S instanceof o?{success:false,error:S}:{success:false,error:new o("TOKEN_INVALID","Invalid refresh token")}}let n=await cr(t,i);if(!n)return {success:false,error:new o("UNAUTHORIZED","Session not found or revoked")};if(n.expiresAt.getTime()<Date.now())return await ae(t,i),{success:false,error:new o("TOKEN_EXPIRED","Session has expired")};await ae(t,i);let d=new Date(Date.now()+O(s.refreshExpiresIn)),a=await ve(t,{userId:n.userId,expiresAt:d}),l={id:n.user.id,identifier:n.user.identifier,identifierType:n.user.identifierType,roles:n.user.roles},g=J({...l,sessionId:a.id},r),v=X(a.id,r);return {success:true,accessToken:g,refreshToken:v,user:l}}async function de(e,r){let s=T(r.dialect),t;try{({sessionId:t}=G(e,r));}catch{return}await ae(s,t);}async function ce(e,r){let s=T(r.dialect);await Te(s,e);}async function fr(e,r){let s=T(r.dialect),t=await ne(s,e);if(!t)return {success:false,error:new o("USER_NOT_FOUND","User not found")};let i=await K(s,e);return {success:true,user:{id:t.id,identifier:t.identifier,identifierType:t.identifierType,roles:t.roles,identifiers:i.map(n=>({id:n.id,type:n.type,value:n.value,isPrimary:n.isPrimary}))}}}async function le(e,r,s,t){let i=A(t),n=T(t.dialect),d=await ne(n,e);if(!d)return {success:false,error:new o("USER_NOT_FOUND","User not found")};if(!await V(r,d.passwordHash))return {success:false,error:new o("INVALID_CREDENTIALS","Invalid credentials")};let l=await M(s,i.saltRounds);return await ur(n,e,l),await Te(n,e),{success:true}}async function fe(e,r,s){let t=A(s),i=T(s.dialect),n=r.filter(g=>!t.validRolesSet.has(g));if(n.length>0)return {success:false,error:new o("INVALID_ROLE",`Invalid roles: ${n.join(", ")}`)};let d=await ne(i,e);if(!d)return {success:false,error:new o("USER_NOT_FOUND","User not found")};let a=new Set(d.roles);for(let g of r)a.add(g);let l=Array.from(a);return await dr(i,e,l),{success:true,user:{id:d.id,identifier:d.identifier,identifierType:d.identifierType,roles:l}}}async function pe(e,r,s){let t=T(s.dialect);if(r.length===0)return {success:false,error:new o("VALIDATION_ERROR","At least one identifier is required")};let i=r.map(l=>({type:l.type.trim(),value:l.value.trim()}));if(new Set(i.map(l=>l.value)).size!==i.length)return {success:false,error:new o("VALIDATION_ERROR","Duplicate identifier values in request")};for(let l of i)if(await Y(t,l.value))return {success:false,error:new o("IDENTIFIER_ALREADY_EXISTS",`Identifier already taken: ${l.value}`)};await sr(t,e,i.map(l=>({...l,isPrimary:false})));return {success:true,identifiers:(await K(t,e)).map(l=>({id:l.id,type:l.type,value:l.value,isPrimary:l.isPrimary}))}}async function me(e,r,s){let t=T(s.dialect);if(r.length===0)return {success:false,error:new o("VALIDATION_ERROR","At least one update is required")};let i=r.map(a=>({id:a.id,type:a.type.trim(),value:a.value.trim()}));if(new Set(i.map(a=>a.value)).size!==i.length)return {success:false,error:new o("VALIDATION_ERROR","Duplicate identifier values in request")};for(let a of i){let l=await oe(t,a.id,e);if(!l)return {success:false,error:new o("IDENTIFIER_NOT_FOUND",`Identifier not found: ${a.id}`)};if(l.value!==a.value&&await Y(t,a.value))return {success:false,error:new o("IDENTIFIER_ALREADY_EXISTS",`Identifier already taken: ${a.value}`)}}for(let a of i)await nr(t,a.id,e,{type:a.type,value:a.value});return {success:true,identifiers:(await K(t,e)).map(a=>({id:a.id,type:a.type,value:a.value,isPrimary:a.isPrimary}))}}async function ye(e,r,s){let t=T(s.dialect);if(r.length===0)return {success:false,error:new o("VALIDATION_ERROR","At least one ID is required")};for(let d of r)if(!await oe(t,d,e))return {success:false,error:new o("IDENTIFIER_NOT_FOUND",`Identifier not found: ${d}`)};return await ir(t,e)-r.length<1?{success:false,error:new o("VALIDATION_ERROR","Cannot delete all identifiers \u2014 at least one must remain")}:(await or(t,e,r),{success:true,identifiers:(await K(t,e)).map(d=>({id:d.id,type:d.type,value:d.value,isPrimary:d.isPrimary}))})}async function ge(e,r,s){let t=T(s.dialect);return await oe(t,r,e)?(await ar(t,e,r),{success:true,identifiers:(await K(t,e)).map(d=>({id:d.id,type:d.type,value:d.value,isPrimary:d.isPrimary}))}):{success:false,error:new o("IDENTIFIER_NOT_FOUND",`Identifier not found: ${r}`)}}function C(e){return e.mode==="client"?Er(e.keyUri):br(e)}function Er(e){return async(r,s,t)=>{let i=r.headers.authorization,n=i?.startsWith("Bearer ")?i.slice(7):void 0;if(!n)return t(new o("UNAUTHORIZED","Missing or malformed Authorization header"));try{let d=await He(e),a=Qe(n,d);r.user={id:a.id,identifier:a.identifier,identifierType:a.identifierType,roles:a.roles},t();}catch(d){t(d);}}}function br(e){return async(r,s,t)=>{let i=Z(r,e);if(!i)return t(new o("UNAUTHORIZED","Missing or malformed Authorization header"));try{let n=se(i,e);if(e.isTokenRevoked&&await e.isTokenRevoked(n.sessionId))return t(new o("UNAUTHORIZED","Token has been revoked"));r.user={id:n.id,identifier:n.identifier,identifierType:n.identifierType,roles:n.roles},t();}catch(n){if(n instanceof o&&n.code==="TOKEN_EXPIRED"){let d=U(r.headers.cookie,D(e));if(!d)return t(new o("UNAUTHORIZED","Token expired. Please login again."));try{let a=await H(d,e);if(!a.success)return t(new o("UNAUTHORIZED","Session expired. Please login again."));z(s,a.refreshToken,e),W(s,a.accessToken,e),s.setHeader("X-New-Access-Token",a.accessToken),r.user=a.user,t();}catch{t(new o("UNAUTHORIZED","Session expired. Please login again."));}}else t(n);}}}function ee(...e){let r=`Requires one of roles: ${e.join(", ")}`;return (s,t,i)=>{if(!s.user)return i(new o("UNAUTHORIZED","Not authenticated"));let n=s.user.roles;if(!e.some(d=>n.includes(d)))return i(new o("FORBIDDEN",r));i();}}var xr=new o("FORBIDDEN","You do not have permission to perform this action");function re(e){let r=typeof e=="function"?{check:e}:e;return async(s,t,i)=>{if(!s.user)return i(new o("UNAUTHORIZED","Not authenticated"));if(r.roles&&r.roles.length>0){let n=s.user.roles;if(r.roles.some(d=>n.includes(d)))return i()}try{let n=r.check(s);(n instanceof Promise?await n:n)?i():i(xr);}catch(n){i(n);}}}function F(e){return (r,s,t,i)=>{if(r instanceof o){t.status(r.statusCode).json({error:true,statusCode:r.statusCode,code:r.code,message:r.message,data:null});return}e?.onUnhandled?.(r),t.status(500).json({error:true,statusCode:500,code:"INTERNAL_SERVER_ERROR",message:"Internal server error",data:null});}}var he=8,j=72,Re=255,pr=100,L=50;function y(e){return new o("VALIDATION_ERROR",e)}function _(e,r,s,t){e.status(r).json({error:false,statusCode:r,message:s,data:t});}function P(e,r){e.status(r.statusCode).json({error:true,statusCode:r.statusCode,code:r.code,message:r.message,data:null});}function N(e){if(e==null||typeof e!="object"||Array.isArray(e))throw new o("VALIDATION_ERROR","Request body is missing or not a JSON object. Did you apply express.json()?");return e}function Nr(e,r){if(!r.apiKey)return;let s=e.headers["x-api-key"];if(typeof s!="string"||s!==r.apiKey)throw new o("UNAUTHORIZED","Invalid or missing API key")}function ke(e){if(e)try{let r=e();r instanceof Promise&&r.catch(()=>{});}catch{}}function Dr(e){return e.startsWith("RS")||e.startsWith("PS")}function Ce(e,r){if(typeof e!="object"||e===null||Array.isArray(e))throw y(`identifiers[${r}] must be an object`);let s=e;if(typeof s.type!="string"||s.type.trim().length===0)throw y(`identifiers[${r}].type is required and must be a non-empty string`);if(s.type.length>pr)throw y(`identifiers[${r}].type must not exceed ${pr} characters`);if(typeof s.value!="string"||s.value.trim().length===0)throw y(`identifiers[${r}].value is required and must be a non-empty string`);if(s.value.length>Re)throw y(`identifiers[${r}].value must not exceed ${Re} characters`);return {type:s.type,value:s.value}}function mr(e){let r=Router(),s=e,t=A(s),i=e.router?.register??(u=>Q(u,s)),n=e.router?.login??(u=>ue(u,s)),d=e.router?.refresh??(u=>H(u,s)),a=e.router?.logout??(u=>u!==void 0?de(u,s):Promise.resolve()),l=e.router?.logoutAll??(u=>ce(u,s)),g=e.router?.assignRoles??((u,c)=>fe(u,c,s)),v=e.router?.changePassword??((u,c,w)=>le(u,c,w,s)),S=e.router?.bulkCreateIdentifiers??((u,c)=>pe(u,c,s)),k=e.router?.bulkUpdateIdentifiers??((u,c)=>me(u,c,s)),R=e.router?.bulkDeleteIdentifiers??((u,c)=>ye(u,c,s)),x=e.router?.changePrimaryIdentifier??((u,c)=>ge(u,c,s));Dr(t.algorithm)&&r.get("/keys",(u,c)=>{c.setHeader("Cache-Control","public, max-age=3600"),c.json(Ue(e.secret));}),r.post("/register",async(u,c,w)=>{try{Nr(u,e);let f=N(u.body),{identifiers:p,password:m,roles:h}=f;if(!Array.isArray(p)||p.length===0)throw y("identifiers is required and must be a non-empty array");if(p.length>L)throw y(`identifiers must not exceed ${L} entries`);let I=p.map(($,wr)=>Ce($,wr));if(typeof m!="string"||m.length<he)throw y(`password is required and must be at least ${he} characters`);if(m.length>j)throw y(`password must not exceed ${j} characters`);if(h!==void 0&&!Array.isArray(h))throw y("roles must be an array of strings when provided");if(Array.isArray(h)&&!h.every($=>typeof $=="string"))throw y("each role must be a string");let b=Array.isArray(h)?h:void 0,q=await i(b!==void 0?{identifiers:I,password:m,roles:b}:{identifiers:I,password:m});if(!q.success){P(c,q.error);return}_(c,201,"User registered successfully",{user:q.user});}catch(f){w(f);}}),r.post("/login",async(u,c,w)=>{try{let f=N(u.body),{identifier:p,password:m}=f;if(typeof p!="string"||p.trim().length===0)throw y("identifier is required and must be a non-empty string");if(p.length>Re)throw y(`identifier must not exceed ${Re} characters`);if(typeof m!="string"||m.length===0)throw y("password is required");if(m.length>j)throw y(`password must not exceed ${j} characters`);let h=p.trim(),I=await n({identifier:h,password:m});if(!I.success){ke(()=>e.hooks?.onFailedLogin?.(h,I.error)),P(c,I.error);return}ke(()=>e.hooks?.onLogin?.(I.user)),z(c,I.refreshToken,e),W(c,I.accessToken,e),_(c,200,"Login successful",{accessToken:I.accessToken,user:I.user});}catch(f){w(f);}}),r.post("/refresh",async(u,c,w)=>{try{let f=U(u.headers.cookie,D(e));if(!f)throw new o("UNAUTHORIZED","Refresh token cookie is missing");let p=await d(f);if(!p.success){ie(c,e),P(c,p.error);return}z(c,p.refreshToken,e),W(c,p.accessToken,e),_(c,200,"Token refreshed",{accessToken:p.accessToken});}catch(f){w(f);}}),r.post("/logout",async(u,c,w)=>{try{let f=U(u.headers.cookie,D(e));await a(f),ie(c,e),Ie(c,e),_(c,200,"Logged out",null);}catch(f){w(f);}}),r.post("/logout-all",C(e),async(u,c,w)=>{try{let f=u.user.id;await l(f),ke(()=>e.hooks?.onLogout?.(f)),ie(c,e),Ie(c,e),_(c,200,"All sessions revoked",null);}catch(f){w(f);}}),r.get("/me",C(e),(u,c)=>{_(c,200,"OK",u.user);});let E=re(u=>!!u.user);return r.post("/me/identifiers",C(e),E,async(u,c,w)=>{try{let f=N(u.body),{identifiers:p}=f;if(!Array.isArray(p)||p.length===0)throw y("identifiers is required and must be a non-empty array");if(p.length>L)throw y(`identifiers must not exceed ${L} entries`);let m=p.map((I,b)=>Ce(I,b)),h=await S(u.user.id,m);if(!h.success){P(c,h.error);return}_(c,201,"Identifiers added successfully",{identifiers:h.identifiers});}catch(f){w(f);}}),r.put("/me/identifiers",C(e),E,async(u,c,w)=>{try{let f=N(u.body),{identifiers:p}=f;if(!Array.isArray(p)||p.length===0)throw y("identifiers is required and must be a non-empty array");if(p.length>L)throw y(`identifiers must not exceed ${L} entries`);let m=p.map((I,b)=>{if(typeof I!="object"||I===null||Array.isArray(I))throw y(`identifiers[${b}] must be an object`);let te=I;if(typeof te.id!="string"||te.id.trim().length===0)throw y(`identifiers[${b}].id is required and must be a non-empty string`);let{type:q,value:$}=Ce(I,b);return {id:te.id,type:q,value:$}}),h=await k(u.user.id,m);if(!h.success){P(c,h.error);return}_(c,200,"Identifiers updated successfully",{identifiers:h.identifiers});}catch(f){w(f);}}),r.delete("/me/identifiers",C(e),E,async(u,c,w)=>{try{let f=N(u.body),{ids:p}=f;if(!Array.isArray(p)||p.length===0)throw y("ids is required and must be a non-empty array of strings");if(!p.every(h=>typeof h=="string"))throw y("each id must be a string");let m=await R(u.user.id,p);if(!m.success){P(c,m.error);return}_(c,200,"Identifiers deleted successfully",{identifiers:m.identifiers});}catch(f){w(f);}}),r.patch("/me/identifiers/primary",C(e),E,async(u,c,w)=>{try{let f=N(u.body),{id:p}=f;if(typeof p!="string"||p.trim().length===0)throw y("id is required and must be a non-empty string");let m=await x(u.user.id,p.trim());if(!m.success){P(c,m.error);return}_(c,200,"Primary identifier updated successfully",{identifiers:m.identifiers});}catch(f){w(f);}}),r.patch("/me/password",C(e),E,async(u,c,w)=>{try{let f=N(u.body),{currentPassword:p,newPassword:m}=f;if(typeof p!="string"||p.length===0)throw y("currentPassword is required");if(typeof m!="string"||m.length<he)throw y(`newPassword must be at least ${he} characters`);if(m.length>j)throw y(`newPassword must not exceed ${j} characters`);if(p===m)throw y("newPassword must be different from currentPassword");let h=await v(u.user.id,p,m);if(!h.success){P(c,h.error);return}_(c,200,"Password updated successfully. All sessions have been revoked.",null);}catch(f){w(f);}}),r.post("/users/:userId/roles",C(e),ee("admin"),async(u,c,w)=>{try{let f=N(u.body),{roles:p}=f,m=u.params.userId,h=typeof m=="string"?m:void 0;if(!h)throw y("userId is required");if(!Array.isArray(p)||p.length===0)throw y("roles must be a non-empty array of strings");if(!p.every(b=>typeof b=="string"))throw y("each role must be a string");let I=await g(h,p);if(!I.success){P(c,I.error);return}_(c,200,"Roles assigned successfully",{user:I.user});}catch(f){w(f);}}),r.use(F()),r}var yr=new Map;function gr(e){let r=yr.get(e);return r||(r=new Redis(e,{lazyConnect:false,enableOfflineQueue:false}),yr.set(e,r)),r}function _e(e){let r=e?.ttl??3e5,s=(e?.header??"X-Idempotency-Key").toLowerCase(),t=new Set((e?.methods??["POST","PUT","PATCH"]).map(n=>n.toUpperCase())),i=e?.redisUrl;return i?Ur(i,r,s,t):Kr(r,s,t,e?.maxSize??1e4)}function Ur(e,r,s,t){let i=gr(e),n="sentri:idempotency:";return async(d,a,l)=>{let g=d.headers[s];if(!g||typeof g!="string"||!t.has(d.method))return l();d.requestId=g,a.setHeader("X-Request-Id",g);let v=await i.get(`${n}${g}`);if(v){let k=JSON.parse(v);return a.setHeader("X-Idempotent-Replayed","true"),a.status(k.statusCode).json(k.body)}let S=a.json.bind(a);a.json=function(R){if(a.statusCode>=200&&a.statusCode<300){let x={statusCode:a.statusCode,body:R,expiresAt:Date.now()+r};i.set(`${n}${g}`,JSON.stringify(x),"PX",r).catch(()=>{});}return S(R)},l();}}function Kr(e,r,s,t){let i=Math.max(e,5e3),n=new Map,d=setInterval(()=>{let a=Date.now();for(let[l,g]of n)g.expiresAt<=a&&n.delete(l);},i);return typeof d=="object"&&d!==null&&"unref"in d&&d.unref(),(a,l,g)=>{let v=a.headers[r];if(!v||typeof v!="string"||!s.has(a.method))return g();a.requestId=v,l.setHeader("X-Request-Id",v);let S=Date.now(),k=n.get(v);if(k&&k.expiresAt>S)return l.setHeader("X-Idempotent-Replayed","true"),l.status(k.statusCode).json(k.body);let R=l.json.bind(l);l.json=function(E){if(l.statusCode>=200&&l.statusCode<300){if(n.size>=t){let u=n.keys().next().value;u!==void 0&&n.delete(u);}n.set(v,{statusCode:l.statusCode,body:E,expiresAt:Date.now()+e});}return R(E)},g();}}async function hr(e){await e.schema.createTable("sentri_users").ifNotExists().addColumn("id","varchar(36)",r=>r.primaryKey().notNull()).addColumn("password_hash","varchar(255)",r=>r.notNull()).addColumn("roles","text",r=>r.notNull().defaultTo("[]")).addColumn("created_at","timestamp",r=>r.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)).execute(),await e.schema.createTable("sentri_sessions").ifNotExists().addColumn("id","varchar(36)",r=>r.primaryKey().notNull()).addColumn("user_id","varchar(36)",r=>r.notNull().references("sentri_users.id").onDelete("cascade")).addColumn("expires_at","timestamp",r=>r.notNull()).addColumn("created_at","timestamp",r=>r.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)).execute(),await e.schema.createTable("sentri_identifiers").ifNotExists().addColumn("id","varchar(36)",r=>r.primaryKey().notNull()).addColumn("user_id","varchar(36)",r=>r.notNull().references("sentri_users.id").onDelete("cascade")).addColumn("type","varchar(100)",r=>r.notNull()).addColumn("value","varchar(255)",r=>r.notNull().unique()).addColumn("is_primary","integer",r=>r.notNull().defaultTo(0)).addColumn("created_at","timestamp",r=>r.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)).execute();}function Ee(e){if(Me(e),e.mode==="client")return {protect:()=>C(e),authorize:(...t)=>ee(...t),permit:t=>re(t),errorHandler:t=>F(t)};let r=e,s=A(r);return {protect:()=>C(r),authorize:(...t)=>ee(...t),permit:t=>re(t),hashPassword:t=>M(t,s.saltRounds),verifyPassword:(t,i)=>V(t,i),signAccessToken:t=>J(t,r),signRefreshToken:t=>X(t,r),verifyAccessToken:t=>se(t,r),verifyRefreshToken:t=>G(t,r),getCurrentAccessToken:t=>Z(t,r),router:()=>mr(r),migrate:()=>hr(T(r.dialect)),errorHandler:t=>F(t),idempotencyMiddleware:t=>_e({...t,...r.redisUrl!==void 0&&{redisUrl:r.redisUrl}}),register:t=>Q(t,r),login:t=>ue(t,r),refresh:t=>H(t,r),logout:t=>de(t,r),logoutAll:t=>ce(t,r),getUser:t=>fr(t,r),changePassword:(t,i,n)=>le(t,i,n,r),assignRoles:(t,i)=>fe(t,i,r),bulkCreateIdentifiers:(t,i)=>pe(t,i,r),bulkUpdateIdentifiers:(t,i)=>me(t,i,r),bulkDeleteIdentifiers:(t,i)=>ye(t,i,r),changePrimaryIdentifier:(t,i)=>ge(t,i,r)}}function Rr(e){return new PostgresDialect({pool:new Pool(e)})}function Lr(e){let{privateKey:r}=generateKeyPairSync("rsa",{modulusLength:2048,privateKeyEncoding:{type:"pkcs8",format:"pem"},publicKeyEncoding:{type:"spki",format:"pem"}}),s={mode:"server",dialect:Rr(e.db),secret:r,algorithm:"RS256",validRoles:e.validRoles,...e.accessExpiresIn!==void 0&&{accessExpiresIn:e.accessExpiresIn},...e.refreshExpiresIn!==void 0&&{refreshExpiresIn:e.refreshExpiresIn},...e.saltRounds!==void 0&&{saltRounds:e.saltRounds},...e.apiKey!==void 0&&{apiKey:e.apiKey},...e.cookie!==void 0&&{cookie:e.cookie},...e.accessCookie!==void 0&&{accessCookie:e.accessCookie},...e.hooks!==void 0&&{hooks:e.hooks},...e.router!==void 0&&{router:e.router},...e.isTokenRevoked!==void 0&&{isTokenRevoked:e.isTokenRevoked},...e.redisUrl!==void 0&&{redisUrl:e.redisUrl}};return Ee(s)}export{be as SENTRI_ERROR_STATUS,o as SentriError,Ee as createAuth,Lr as createAuthServer,F as createErrorHandler,_e as createIdempotencyMiddleware,Z as getCurrentAccessToken,Q as register};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentri",
3
- "version": "2.1.0",
3
+ "version": "4.0.0",
4
4
  "description": "Personal auth/authorization library for Express + Postgres",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",