sentri 4.1.1 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,320 @@
1
+ import * as kysely from 'kysely';
2
+ import { RequestHandler, Request, ErrorRequestHandler, Router } from 'express';
3
+ import { AuthConfig, SentriLogger, ServerAuthConfig, AuthUser, CookieConfig, AccessCookieConfig, AuthHooks, RouterHandlers, RegisterInput, RegisterResult, LoginInput, AuthResult, RefreshResult, GetUserResult, ChangePasswordResult, AssignRolesResult, IdentifierInput, BulkIdentifiersResult } from '../../core/index.js';
4
+ export { ApiResponse, ClientAuthConfig, IdentifierRecord, SENTRI_ERROR_STATUS, SentriError, SentriErrorCode } from '../../core/index.js';
5
+
6
+ declare function protect(config: AuthConfig): RequestHandler;
7
+
8
+ /**
9
+ * Returns a logger-aware `authorize` factory bound to the given logger and service name.
10
+ * Used internally by `createAuth()` and `createAuthRouter()` to inject logging.
11
+ * Not part of the public API.
12
+ *
13
+ * @internal
14
+ */
15
+ declare function createAuthorize(logger: SentriLogger, service: string): <TRole extends string>(...allowedRoles: TRole[]) => RequestHandler;
16
+ /**
17
+ * Express middleware factory for role-based access control (RBAC).
18
+ *
19
+ * Passes if the authenticated user (set by `protect()`) has **at least one**
20
+ * of the specified `allowedRoles`. Calls `next(SentriError)` with code `FORBIDDEN`
21
+ * if no roles match, or `UNAUTHORIZED` if `req.user` is absent.
22
+ *
23
+ * Must be used **after** `protect()`.
24
+ *
25
+ * > **Logging** — when used via `auth.authorize()` from `createAuth()`, log entries
26
+ * > are emitted automatically using the logger configured in `AuthConfig`.
27
+ * > When called directly (standalone), no logging occurs.
28
+ *
29
+ * @param allowedRoles - One or more role strings. The user needs at least one of them.
30
+ * @returns An Express `RequestHandler` that enforces the role check.
31
+ *
32
+ * @example
33
+ * router.delete('/posts/:id', protect(config), authorize('admin', 'moderator'), handler);
34
+ */
35
+ declare function authorize<TRole extends string>(...allowedRoles: TRole[]): RequestHandler;
36
+
37
+ /** A function that determines whether the current request is permitted. */
38
+ type PermitCheck = (request: Request) => boolean | Promise<boolean>;
39
+ /**
40
+ * Options for {@link permit} when you need role-bypass alongside a resource check.
41
+ *
42
+ * @example
43
+ * // Admins can edit any post; others only their own
44
+ * auth.permit({
45
+ * roles: ['admin'],
46
+ * check: async (request) => {
47
+ * const post = await db.findPost(request.params['id']);
48
+ * return post?.authorId === request.user!.id;
49
+ * },
50
+ * })
51
+ */
52
+ interface PermitOptions<TRole extends string> {
53
+ /**
54
+ * Roles whose members are granted access without running `check`.
55
+ * Use for privileged roles like `'admin'` that should bypass ownership checks.
56
+ */
57
+ roles?: TRole[];
58
+ /**
59
+ * Permission check executed when the user has none of the bypass `roles`.
60
+ * Return `true` to allow, `false` to deny with `FORBIDDEN`.
61
+ * May be async — useful for database-backed ownership checks.
62
+ */
63
+ check: PermitCheck;
64
+ }
65
+ /**
66
+ * Returns a logger-aware `permit` factory bound to the given logger and service name.
67
+ * Used internally by `createAuth()` and `createAuthRouter()` to inject logging.
68
+ * Not part of the public API.
69
+ *
70
+ * @internal
71
+ */
72
+ declare function createPermit(logger: SentriLogger, service: string): <TRole extends string>(optionsOrCheck: PermitOptions<TRole> | PermitCheck) => RequestHandler;
73
+ /**
74
+ * Express middleware factory for resource-level permission checks.
75
+ *
76
+ * Must be used **after** `protect()`. Evaluates a check function against the
77
+ * current request; calls `next(SentriError)` with code `FORBIDDEN` if it returns `false`.
78
+ *
79
+ * Accepts either a bare check function or an options object with an optional
80
+ * `roles` list whose members bypass the check entirely.
81
+ *
82
+ * **Performance:** when `check` returns a plain `boolean` (not a Promise) no
83
+ * extra microtask is queued — the result is handled synchronously. Async checks
84
+ * are awaited normally.
85
+ *
86
+ * > **Logging** — when used via `auth.permit()` from `createAuth()`, log entries
87
+ * > are emitted automatically using the logger configured in `AuthConfig`.
88
+ * > When called directly (standalone), no logging occurs.
89
+ *
90
+ * @example
91
+ * // Simple ownership check (sync — zero async overhead)
92
+ * router.put('/users/:id',
93
+ * auth.protect(),
94
+ * auth.permit((request) => request.user!.id === request.params['id']),
95
+ * updateUserHandler,
96
+ * );
97
+ *
98
+ * @example
99
+ * // Admins bypass the check; others must own the post (async DB lookup)
100
+ * router.delete('/posts/:id',
101
+ * auth.protect(),
102
+ * auth.permit({
103
+ * roles: ['admin'],
104
+ * check: async (request) => {
105
+ * const post = await db.post.findUnique({ where: { id: request.params['id'] } });
106
+ * return post?.authorId === request.user!.id;
107
+ * },
108
+ * }),
109
+ * deletePostHandler,
110
+ * );
111
+ */
112
+ declare function permit<TRole extends string>(optionsOrCheck: PermitOptions<TRole> | PermitCheck): RequestHandler;
113
+
114
+ /**
115
+ * Options for {@link createErrorHandler}.
116
+ */
117
+ interface ErrorHandlerOptions {
118
+ /**
119
+ * Called for errors that are **not** a `SentriError` instance (or subclass).
120
+ *
121
+ * Use this to log unexpected server errors before the generic 500 response
122
+ * is sent. The error is passed as-is and may be any unknown value.
123
+ *
124
+ * @example
125
+ * app.use(auth.errorHandler({
126
+ * onUnhandled: (err) => logger.error('Unhandled error', { err }),
127
+ * }));
128
+ */
129
+ onUnhandled?: (error: unknown) => void;
130
+ }
131
+ /**
132
+ * Creates an Express error-handling middleware that formats every `SentriError`
133
+ * (including subclasses) into the standard sentri response envelope:
134
+ *
135
+ * ```json
136
+ * { "error": true, "statusCode": 401, "code": "UNAUTHORIZED", "message": "...", "data": null }
137
+ * ```
138
+ *
139
+ * Prefer using `auth.errorHandler()` instead of calling this directly:
140
+ *
141
+ * ```typescript
142
+ * app.use('/auth', auth.router());
143
+ * app.use('/api', apiRouter);
144
+ *
145
+ * // Must come after all route/middleware registrations
146
+ * app.use(auth.errorHandler());
147
+ * ```
148
+ *
149
+ * ---
150
+ *
151
+ * **Works with built-in sentri errors and your own subclasses**
152
+ *
153
+ * Because `instanceof SentriError` matches any subclass, you can define
154
+ * application-specific error types and have them automatically formatted
155
+ * by this handler:
156
+ *
157
+ * ```typescript
158
+ * import { SentriError } from 'sentri';
159
+ *
160
+ * // Extend SentriError for domain-specific failures
161
+ * export class NotFoundError extends SentriError {
162
+ * constructor(resource: string) {
163
+ * super('NOT_FOUND', `${resource} not found`, 404);
164
+ * }
165
+ * }
166
+ *
167
+ * export class PaymentError extends SentriError {
168
+ * constructor(message: string) {
169
+ * super('PAYMENT_FAILED', message, 402);
170
+ * }
171
+ * }
172
+ *
173
+ * // All of the above are caught and formatted by one handler
174
+ * app.use(auth.errorHandler({
175
+ * onUnhandled: (err) => console.error('Unexpected error:', err),
176
+ * }));
177
+ * ```
178
+ *
179
+ * @param options - Optional configuration (see {@link ErrorHandlerOptions}).
180
+ * @returns An Express `ErrorRequestHandler` (4-argument middleware).
181
+ */
182
+ declare function createErrorHandler(options?: ErrorHandlerOptions): ErrorRequestHandler;
183
+
184
+ interface IdempotencyOptions {
185
+ /** @default 300_000 (5 minutes) */
186
+ ttl?: number;
187
+ /** @default 'X-Idempotency-Key' */
188
+ header?: string;
189
+ /** @default ['POST', 'PUT', 'PATCH'] */
190
+ methods?: string[];
191
+ /**
192
+ * Max in-memory entries (ignored when redisUrl is set).
193
+ * @default 10_000
194
+ */
195
+ maxSize?: number;
196
+ /**
197
+ * Redis connection URL (e.g. `redis://localhost:6379`).
198
+ * When set, uses Redis as the cache backend instead of in-memory Map.
199
+ */
200
+ redisUrl?: string;
201
+ }
202
+ /**
203
+ * Middleware that deduplicates non-idempotent requests.
204
+ *
205
+ * When a request arrives with a matching idempotency key header, the cached
206
+ * response is replayed immediately — the handler is not called again.
207
+ * Responses are only cached for 2xx status codes.
208
+ *
209
+ * Two backends are available:
210
+ * - **In-memory** (default) — zero dependencies, single-process only.
211
+ * - **Redis** — set `redisUrl` to share state across processes/instances.
212
+ *
213
+ * When using `createAuthServer()`, prefer `auth.idempotencyMiddleware()` instead —
214
+ * it automatically inherits the `redisUrl` from server config.
215
+ *
216
+ * @example
217
+ * // Standalone usage
218
+ * app.use(createIdempotencyMiddleware({ ttl: 60_000 }));
219
+ *
220
+ * @example
221
+ * // Multi-process (Redis)
222
+ * app.use(createIdempotencyMiddleware({ redisUrl: 'redis://localhost:6379' }));
223
+ */
224
+ declare function createIdempotencyMiddleware(options?: IdempotencyOptions): RequestHandler;
225
+
226
+ /**
227
+ * Extract the raw access token string from an Express request.
228
+ * Reads `Authorization: Bearer <token>` header first; falls back to the
229
+ * `access_token` cookie (or the name set in `accessCookie.name`).
230
+ * Returns `undefined` when no token is present.
231
+ */
232
+ declare function getCurrentAccessToken(request: Request, config: ServerAuthConfig): string | undefined;
233
+
234
+ declare function createAuthRouter<TRole extends string>(config: ServerAuthConfig<TRole>): Router;
235
+
236
+ declare global {
237
+ namespace Express {
238
+ interface Request {
239
+ user?: AuthUser;
240
+ requestId?: string;
241
+ }
242
+ }
243
+ }
244
+ interface CreateExpressServerOptions<TRole extends string = string> {
245
+ mode: 'server';
246
+ validRoles: readonly TRole[];
247
+ validIdentifiers?: readonly string[];
248
+ dialect: kysely.Dialect;
249
+ accessExpiresIn?: string | number;
250
+ refreshExpiresIn?: string | number;
251
+ saltRounds?: number;
252
+ apiKey?: string;
253
+ cookie?: CookieConfig;
254
+ accessCookie?: AccessCookieConfig;
255
+ hooks?: AuthHooks;
256
+ router?: RouterHandlers;
257
+ isTokenRevoked?: (sessionId: string) => boolean | Promise<boolean>;
258
+ redisUrl?: string;
259
+ logger?: SentriLogger;
260
+ loggerService?: string;
261
+ }
262
+ interface CreateExpressClientOptions<TRole extends string = string> {
263
+ mode: 'client';
264
+ keyUri: string;
265
+ validRoles?: readonly TRole[];
266
+ logger?: SentriLogger;
267
+ loggerService?: string;
268
+ }
269
+ interface AuthExpressClient<TRole extends string = string> {
270
+ /** Middleware that ensures the request has a valid access token. Attaches `req.user`. */
271
+ protect(): RequestHandler;
272
+ /** Middleware that ensures the user has AT LEAST ONE of the required roles. */
273
+ authorize(...roles: TRole[]): RequestHandler;
274
+ /** Middleware that evaluates a custom authorization condition or ABAC rules. */
275
+ permit(check: PermitCheck): RequestHandler;
276
+ permit(options: PermitOptions<TRole>): RequestHandler;
277
+ /** Express error handler that converts Sentri errors to standardized JSON responses. */
278
+ errorHandler(options?: ErrorHandlerOptions): ErrorRequestHandler;
279
+ }
280
+ interface ServerAuthExpressClient<TRole extends string = string> extends AuthExpressClient<TRole> {
281
+ hashPassword(plain: string): Promise<string>;
282
+ verifyPassword(plain: string, hash: string): Promise<boolean>;
283
+ signAccessToken(payload: AuthUser<TRole>): string;
284
+ signRefreshToken(sessionId: string): string;
285
+ verifyAccessToken(token: string): AuthUser<TRole>;
286
+ verifyRefreshToken(token: string): {
287
+ sessionId: string;
288
+ };
289
+ getCurrentAccessToken(request: Request): string | undefined;
290
+ /** Returns an Express Router containing all auth endpoints (e.g. POST /register). */
291
+ router(): Router;
292
+ /** Runs Kysely database migrations for Sentri tables. */
293
+ migrate(): Promise<void>;
294
+ /** Middleware to prevent duplicate requests using idempotency keys. */
295
+ idempotencyMiddleware(options?: IdempotencyOptions): RequestHandler;
296
+ register(input: RegisterInput<TRole>): Promise<RegisterResult<TRole>>;
297
+ login(input: LoginInput): Promise<AuthResult<TRole>>;
298
+ refresh(refreshToken: string): Promise<RefreshResult<TRole>>;
299
+ logout(refreshToken: string): Promise<void>;
300
+ logoutAll(userId: string): Promise<void>;
301
+ getUser(userId: string): Promise<GetUserResult<TRole>>;
302
+ changePassword(userId: string, currentPassword: string, newPassword: string): Promise<ChangePasswordResult>;
303
+ assignRoles(userId: string, roles: TRole[]): Promise<AssignRolesResult<TRole>>;
304
+ bulkCreateIdentifiers(userId: string, identifiers: IdentifierInput[]): Promise<BulkIdentifiersResult>;
305
+ bulkUpdateIdentifiers(userId: string, updates: Array<{
306
+ id: string;
307
+ type: string;
308
+ value: string;
309
+ }>): Promise<BulkIdentifiersResult>;
310
+ bulkDeleteIdentifiers(userId: string, ids: string[]): Promise<BulkIdentifiersResult>;
311
+ }
312
+ type ClientAuthExpressClient<TRole extends string = string> = AuthExpressClient<TRole>;
313
+ declare function createAuthExpress<TRole extends string = string>(options: CreateExpressServerOptions<TRole>): ServerAuthExpressClient<TRole>;
314
+ /**
315
+ * Creates an Express authentication client (Client Mode).
316
+ * Used for microservices that only verify tokens issued by a separate auth server.
317
+ */
318
+ declare function createAuthExpress<TRole extends string = string>(options: CreateExpressClientOptions<TRole>): ClientAuthExpressClient<TRole>;
319
+
320
+ export { AccessCookieConfig, AssignRolesResult, AuthConfig, type AuthExpressClient, AuthHooks, AuthResult, AuthUser, BulkIdentifiersResult, ChangePasswordResult, type ClientAuthExpressClient, CookieConfig, type CreateExpressClientOptions, type CreateExpressServerOptions, type ErrorHandlerOptions, GetUserResult, type IdempotencyOptions, IdentifierInput, LoginInput, type PermitCheck, type PermitOptions, RefreshResult, RegisterInput, RegisterResult, RouterHandlers, SentriLogger, ServerAuthConfig, type ServerAuthExpressClient, authorize, createAuthExpress, createAuthRouter, createAuthorize, createErrorHandler, createIdempotencyMiddleware, createPermit, getCurrentAccessToken, permit, protect };
@@ -0,0 +1 @@
1
+ import {generateKeyPairSync,createPrivateKey,createPublicKey,createHash,randomUUID}from'crypto';import {sql,Kysely}from'kysely';import Ye from'bcrypt';import Q from'jsonwebtoken';import {Redis}from'ioredis';import {Router}from'express';var Kr=Object.defineProperty;var n=(e,r)=>Kr(e,"name",{value:r,configurable:true});var je=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,TOKEN_REUSE:401}),l=class extends Error{static{n(this,"SentriError");}code;statusCode;constructor(r,t,s){super(t),this.name="SentriError",this.code=r,this.statusCode=s??je[r]??500;}};var Ve=new WeakMap,Be=32,Xe=10,ze=31;function Ge(e){if(e.mode==="client"){if(!e.keyUri||e.keyUri.trim().length===0)throw new l("CONFIGURATION_ERROR","keyUri must not be empty");return}if(!e.secret||e.secret.trim().length===0)throw new l("CONFIGURATION_ERROR","secret must not be empty");if((e.algorithm??"HS256").startsWith("HS")&&e.secret.length<Be)throw new l("CONFIGURATION_ERROR",`secret must be at least ${Be} characters for HMAC algorithms`);let s=e.saltRounds??12;if(!Number.isInteger(s)||s<Xe||s>ze)throw new l("CONFIGURATION_ERROR",`saltRounds must be an integer between ${Xe} and ${ze}`);if(!e.validRoles||e.validRoles.length===0)throw new l("CONFIGURATION_ERROR","validRoles must contain at least one role");if(e.validIdentifiers!==void 0&&e.validIdentifiers.length===0)throw new l("CONFIGURATION_ERROR","validIdentifiers must contain at least one identifier type");if(!e.dialect)throw new l("CONFIGURATION_ERROR","dialect is required in server mode")}n(Ge,"validateConfig");function k(e){let r=Ve.get(e);if(r)return r;let t={algorithm:e.algorithm??"HS256",accessExpiresIn:e.accessExpiresIn??"15m",refreshExpiresIn:e.refreshExpiresIn??"7d",saltRounds:e.saltRounds??12,validRoles:e.validRoles,validRolesSet:new Set(e.validRoles),validIdentifiers:e.validIdentifiers??["email","username"],validIdentifiersSet:new Set(e.validIdentifiers??["email","username"]),cookieName:e.cookie?.name??"refresh_token",accessCookieName:e.accessCookie?.name??"access_token"};return Ve.set(e,t),t}n(k,"resolveServerConfig");var $r=/^(\d+)([smhdw])$/,Hr={s:1e3,m:6e4,h:36e5,d:864e5,w:6048e5},Je=new Map;function V(e){if(typeof e=="number")return e*1e3;let r=Je.get(e);if(r!==void 0)return r;let t=$r.exec(e);if(!t?.[1]||!t?.[2])throw new Error(`Invalid expiresIn: "${e}". Use e.g. "15m", "7d", "1h".`);let s=Hr[t[2]];if(s===void 0)throw new Error(`Invalid expiresIn: "${e}". Use e.g. "15m", "7d", "1h".`);let i=parseInt(t[1],10)*s;return Je.set(e,i),i}n(V,"parseExpiry");var L={info:n(()=>{},"info"),warn:n(()=>{},"warn"),error:n(()=>{},"error")};function p(e,r,t){return {service:e,event:r,...t}}n(p,"buildLogData");async function We(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("ip_address","varchar(45)").addColumn("user_agent","text").addColumn("replaced_by","varchar(36)").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("created_at","timestamp",r=>r.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)).execute(),await e.schema.createIndex("idx_sentri_sessions_user_id").ifNotExists().on("sentri_sessions").column("user_id").execute(),await e.schema.createIndex("idx_sentri_identifiers_user_id").ifNotExists().on("sentri_identifiers").column("user_id").execute();}n(We,"runMigrations");var Ze=new Map;function D(e){let r=Ze.get(e);return r||(r=new Kysely({dialect:e}),Ze.set(e,r)),r}n(D,"getDatabase");async function Y(e,r=12){return Ye.hash(e,r)}n(Y,"hashPassword");async function q(e,r){return Ye.compare(e,r)}n(q,"verifyPassword");var qe=new Map,Qe=new Map,Br=3600*1e3;function rr(e){let r=qe.get(e);if(!r){let t=createPrivateKey(e),s=createPublicKey(t),i=s.export({format:"jwk"}),f=createHash("sha256").update(JSON.stringify({e:i.e,kty:i.kty,n:i.n})).digest("base64url"),c={...i,use:"sig",kid:f};r={kid:f,publicKey:s,jwk:c},qe.set(e,r);}return r}n(rr,"getOrBuildKey");function tr(e){let{jwk:r}=rr(e);return {keys:[r]}}n(tr,"buildJwks");function sr(e){return rr(e).publicKey}n(sr,"getPublicKeyFromPrivate");async function nr(e){let r=Date.now(),t=Qe.get(e);if(t&&r-t.fetchedAt<Br)return t.publicKey;let s=await fetch(e);if(!s.ok)throw new l("CONFIGURATION_ERROR",`Failed to fetch public key from ${e}: HTTP ${s.status}`);let i=await s.json();if(!i.keys||i.keys.length===0)throw new l("CONFIGURATION_ERROR",`No keys found in JWKS response from ${e}`);let o=i.keys[0],f=createPublicKey({key:o,format:"jwk"});return Qe.set(e,{publicKey:f,fetchedAt:r}),f}n(nr,"fetchPublicKey");var ir=new Map,or=new Map,ar=new Map;function dr(e){return e.startsWith("RS")||e.startsWith("PS")}n(dr,"isRSA");function ur(e){let r=ir.get(e);return r||(r={access:`${e}:access`,refresh:`${e}:refresh`},ir.set(e,r)),r}n(ur,"getHsSecrets");function zr(e){let r=or.get(e);return r||(r=createPrivateKey(e),or.set(e,r)),r}n(zr,"getRsPrivateKey");function cr(e){let r=k(e);if(dr(r.algorithm)){let i=zr(e.secret);return {accessKey:i,refreshKey:i}}let{access:t,refresh:s}=ur(e.secret);return {accessKey:t,refreshKey:s}}n(cr,"getSigningKeys");function lr(e,r){let t=k(e);if(dr(t.algorithm))return sr(e.secret);let{access:s,refresh:i}=ur(e.secret);return r==="access"?s:i}n(lr,"getVerifyKey");function fr(e,r,t,s){let i=`${t}:${s}`,o=ar.get(i);return o||(o={expiresIn:t,algorithm:s},ar.set(i,o)),Q.sign(e,r,o)}n(fr,"sign");function hr(e,r,t){try{let s=Q.verify(e,r,{algorithms:[t]});if(typeof s=="string"||s===null)throw new l("TOKEN_INVALID","Token payload is not an object");return s}catch(s){throw s instanceof l?s:s instanceof Q.TokenExpiredError?new l("TOKEN_EXPIRED","Token has expired"):new l("TOKEN_INVALID","Token is invalid or malformed")}}n(hr,"verify");function ee(e,r){let t=k(r),{accessKey:s}=cr(r);return fr(e,s,t.accessExpiresIn,t.algorithm)}n(ee,"signAccessToken");function re(e,r){let t=k(r),{refreshKey:s}=cr(r);return fr({sessionId:e},s,t.refreshExpiresIn,t.algorithm)}n(re,"signRefreshToken");function fe(e,r){let t=k(r),s=lr(r,"access");return hr(e,s,t.algorithm)}n(fe,"verifyAccessToken");function te(e,r){let t=k(r),s=lr(r,"refresh");return hr(e,s,t.algorithm)}n(te,"verifyRefreshToken");function mr(e,r){try{let t=Q.verify(e,r,{algorithms:["RS256","RS384","RS512","PS256","PS384","PS512"]});if(typeof t=="string"||t===null)throw new l("TOKEN_INVALID","Token payload is not an object");return t}catch(t){throw t instanceof l?t:t instanceof Q.TokenExpiredError?new l("TOKEN_EXPIRED","Token has expired"):new l("TOKEN_INVALID","Token is invalid or malformed")}}n(mr,"verifyTokenWithPublicKey");function Ue(e){try{return JSON.parse(e)}catch{return []}}n(Ue,"parseRoles");function Jr(e){return JSON.stringify(e)}n(Jr,"serializeRoles");function wr(e){return {id:e.id,userId:e.user_id,type:e.type,value:e.value,createdAt:new Date(e.created_at)}}n(wr,"mapIdentifier");async function se(e,r){let t=await e.selectFrom("sentri_identifiers as i").innerJoin("sentri_users as u","u.id","i.user_id").select(["u.id","u.password_hash","u.roles"]).where("i.value","=",r).executeTakeFirst();return t?{id:t.id,passwordHash:t.password_hash,roles:Ue(t.roles)}:null}n(se,"findUserByIdentifierValue");async function he(e,r){let t=await e.selectFrom("sentri_users").select(["id","password_hash","roles"]).where("id","=",r).executeTakeFirst();return t?{id:t.id,passwordHash:t.password_hash,roles:Ue(t.roles)}:null}n(he,"findUserById");async function yr(e,r,t){let s=t.map(i=>({id:randomUUID(),user_id:r,type:i.type,value:i.value}));return await e.insertInto("sentri_identifiers").values(s).execute(),s.map(i=>({id:i.id,userId:i.user_id,type:i.type,value:i.value,createdAt:new Date}))}n(yr,"createIdentifiers");async function ne(e,r){return (await e.selectFrom("sentri_identifiers").selectAll().where("user_id","=",r).orderBy("created_at","asc").execute()).map(wr)}n(ne,"findIdentifiersByUserId");async function Le(e,r,t){let s=await e.selectFrom("sentri_identifiers").selectAll().where("id","=",r).where("user_id","=",t).executeTakeFirst();return s?wr(s):null}n(Le,"findIdentifierById");async function Ir(e,r){let t=await e.selectFrom("sentri_identifiers").select(s=>s.fn.countAll().as("count")).where("user_id","=",r).executeTakeFirst();return Number(t?.count??0)}n(Ir,"countIdentifiersByUserId");async function Rr(e,r,t,s){await e.updateTable("sentri_identifiers").set({type:s.type,value:s.value}).where("id","=",r).where("user_id","=",t).execute();}n(Rr,"updateIdentifier");async function _r(e,r,t){await e.deleteFrom("sentri_identifiers").where("user_id","=",r).where("id","in",t).execute();}n(_r,"deleteIdentifiers");async function gr(e,r,t){await e.updateTable("sentri_users").set({password_hash:t}).where("id","=",r).execute();}n(gr,"updateUserPassword");async function vr(e,r,t){await e.updateTable("sentri_users").set({roles:Jr(t)}).where("id","=",r).execute();}n(vr,"updateUserRoles");async function Fe(e,r){let t=randomUUID();return await e.insertInto("sentri_sessions").values({id:t,user_id:r.userId,expires_at:r.expiresAt.toISOString(),ip_address:r.ipAddress??null,user_agent:r.userAgent??null}).execute(),{id:t}}n(Fe,"createSession");async function me(e,r){let t=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.ip_address","s.user_agent","s.replaced_by","s.created_at as session_created_at","u.id as user_id_col","u.password_hash","u.roles"]).where("s.id","=",r).executeTakeFirst();return t?{id:t.session_id,userId:t.user_id,expiresAt:new Date(t.expires_at),ipAddress:t.ip_address,userAgent:t.user_agent,replacedBy:t.replaced_by,createdAt:new Date(t.session_created_at),user:{id:t.user_id_col,passwordHash:t.password_hash,roles:Ue(t.roles)}}:null}n(me,"findSessionById");async function pe(e,r){await e.deleteFrom("sentri_sessions").where("id","=",r).execute();}n(pe,"deleteSession");async function Ar(e,r,t){await e.updateTable("sentri_sessions").set({replaced_by:t}).where("id","=",r).execute();}n(Ar,"markSessionReplaced");async function we(e,r){await e.deleteFrom("sentri_sessions").where("user_id","=",r).execute();}n(we,"deleteAllSessionsForUser");async function kr(e,r){return (await e.selectFrom("sentri_sessions").selectAll().where("user_id","=",r).where("replaced_by","is",null).where("expires_at",">",new Date).execute()).map(s=>({id:s.id,userId:s.user_id,expiresAt:new Date(s.expires_at),ipAddress:s.ip_address,userAgent:s.user_agent,replacedBy:s.replaced_by,createdAt:new Date(s.created_at)}))}n(kr,"findSessionsByUserId");async function ye(e,r,t){let s=k(r),i=D(r.dialect),o=e.roles??[],f=o.filter(A=>!s.validRolesSet.has(A));if(f.length>0)return {success:false,error:new l("INVALID_ROLE",`Invalid roles: ${f.join(", ")}`)};if(!e.identifiers||e.identifiers.length===0)return {success:false,error:new l("VALIDATION_ERROR","At least one identifier is required")};let c=e.identifiers.map(A=>({type:A.type.trim(),value:A.value.trim()})),a=c.filter(A=>!s.validIdentifiersSet.has(A.type));if(a.length>0)return {success:false,error:new l("VALIDATION_ERROR",`Invalid identifier types: ${a.map(A=>A.type).join(", ")}`)};if(new Set(c.map(A=>A.value)).size!==c.length)return {success:false,error:new l("VALIDATION_ERROR","Duplicate identifier values in request")};for(let A of c)if(await se(i,A.value))return {success:false,error:new l("IDENTIFIER_ALREADY_EXISTS",`Identifier already taken: ${A.value}`)};let w=await Y(e.password,s.saltRounds),{userId:C,identifierRows:T}=await i.transaction().execute(async A=>{let P=randomUUID();await A.insertInto("sentri_users").values({id:P,password_hash:w,roles:JSON.stringify(o)}).execute();let $=c.map(ce=>({id:randomUUID(),user_id:P,type:ce.type,value:ce.value}));return await A.insertInto("sentri_identifiers").values($).execute(),{userId:P,identifierRows:$}}),S={success:true,user:{id:C,roles:o,identifiers:T.map(A=>({id:A.id,type:A.type,value:A.value}))}};return r.hooks?.onRegister&&await r.hooks.onRegister(S.user),S}n(ye,"register");async function Ie(e,r,t){let s=k(r),i=D(r.dialect),o=await se(i,e.identifier.trim());if(!o){let T=new l("INVALID_CREDENTIALS","Invalid credentials");return r.hooks?.onLoginFailed&&await r.hooks.onLoginFailed(e.identifier,T,{ip:t?.ip??""}),{success:false,error:T}}if(!await q(e.password,o.passwordHash)){let T=new l("INVALID_CREDENTIALS","Invalid credentials");return r.hooks?.onLoginFailed&&await r.hooks.onLoginFailed(e.identifier,T,{ip:t?.ip??""}),{success:false,error:T}}let c=new Date(Date.now()+V(s.refreshExpiresIn)),a=await Fe(i,{userId:o.id,expiresAt:c,ipAddress:t?.ip??null,userAgent:t?.userAgent??null}),u={id:o.id,roles:o.roles},w=ee({id:o.id,roles:o.roles,sessionId:a.id},r),C=re(a.id,r);return r.hooks?.onLoginSuccess&&await r.hooks.onLoginSuccess(u,{ip:t?.ip??"",userAgent:t?.userAgent??""}),{success:true,accessToken:w,refreshToken:C,user:u}}n(Ie,"login");async function B(e,r,t){let s=k(r),i=D(r.dialect),o;try{({sessionId:o}=te(e,r));}catch(T){return T instanceof l?{success:false,error:T}:{success:false,error:new l("TOKEN_INVALID","Invalid refresh token")}}let f=await me(i,o);if(!f)return {success:false,error:new l("UNAUTHORIZED","Session not found or revoked")};if(f.replacedBy)return await we(i,f.userId),{success:false,error:new l("TOKEN_REUSE","Token reuse detected. All sessions revoked.")};if(f.expiresAt.getTime()<Date.now())return await pe(i,o),{success:false,error:new l("TOKEN_EXPIRED","Session has expired")};let c=new Date(Date.now()+V(s.refreshExpiresIn)),a=await Fe(i,{userId:f.userId,expiresAt:c,ipAddress:t?.ip??null,userAgent:t?.userAgent??null});await Ar(i,o,a.id);let u={id:f.user.id,roles:f.user.roles},w=ee({...u,sessionId:a.id},r),C=re(a.id,r);return {success:true,accessToken:w,refreshToken:C,user:u}}n(B,"refresh");async function Re(e,r){let t=D(r.dialect),s;try{({sessionId:s}=te(e,r));}catch{return}let i=await me(t,s);i&&(await pe(t,s),r.hooks?.onLogout&&await r.hooks.onLogout(i.userId));}n(Re,"logout");async function _e(e,r){let t=D(r.dialect);await we(t,e),r.hooks?.onLogout&&await r.hooks.onLogout(e);}n(_e,"logoutAll");async function ge(e,r){let t=D(r.dialect),s=await he(t,e);if(!s)return {success:false,error:new l("USER_NOT_FOUND","User not found")};let i=await ne(t,e);return {success:true,user:{id:s.id,roles:s.roles,identifiers:i.map(o=>({id:o.id,type:o.type,value:o.value}))}}}n(ge,"getUser");async function ve(e,r,t,s){let i=k(s),o=D(s.dialect),f=await he(o,e);if(!f)return {success:false,error:new l("USER_NOT_FOUND","User not found")};if(!await q(r,f.passwordHash))return {success:false,error:new l("INVALID_CREDENTIALS","Invalid credentials")};let a=await Y(t,i.saltRounds);return await gr(o,e,a),await we(o,e),s.hooks?.onPasswordChanged&&await s.hooks.onPasswordChanged(e),{success:true}}n(ve,"changePassword");async function Ae(e,r,t){let s=k(t),i=D(t.dialect),o=r.filter(u=>!s.validRolesSet.has(u));if(o.length>0)return {success:false,error:new l("INVALID_ROLE",`Invalid roles: ${o.join(", ")}`)};let f=await he(i,e);if(!f)return {success:false,error:new l("USER_NOT_FOUND","User not found")};let c=new Set(f.roles);for(let u of r)c.add(u);let a=Array.from(c);return await vr(i,e,a),{success:true,user:{id:f.id,roles:a}}}n(Ae,"assignRoles");async function ke(e,r,t){let s=k(t),i=D(t.dialect);if(r.length===0)return {success:false,error:new l("VALIDATION_ERROR","At least one identifier is required")};let o=r.map(u=>({type:u.type.trim(),value:u.value.trim()})),f=o.filter(u=>!s.validIdentifiersSet.has(u.type));if(f.length>0)return {success:false,error:new l("VALIDATION_ERROR",`Invalid identifier types: ${f.map(u=>u.type).join(", ")}`)};if(new Set(o.map(u=>u.value)).size!==o.length)return {success:false,error:new l("VALIDATION_ERROR","Duplicate identifier values in request")};for(let u of o)if(await se(i,u.value))return {success:false,error:new l("IDENTIFIER_ALREADY_EXISTS",`Identifier already taken: ${u.value}`)};return await yr(i,e,o),{success:true,identifiers:(await ne(i,e)).map(u=>({id:u.id,type:u.type,value:u.value}))}}n(ke,"bulkCreateIdentifiers");async function Ee(e,r,t){let s=k(t),i=D(t.dialect);if(r.length===0)return {success:false,error:new l("VALIDATION_ERROR","At least one update is required")};let o=r.map(u=>({id:u.id,type:u.type.trim(),value:u.value.trim()})),f=o.filter(u=>!s.validIdentifiersSet.has(u.type));if(f.length>0)return {success:false,error:new l("VALIDATION_ERROR",`Invalid identifier types: ${f.map(u=>u.type).join(", ")}`)};if(new Set(o.map(u=>u.value)).size!==o.length)return {success:false,error:new l("VALIDATION_ERROR","Duplicate identifier values in request")};for(let u of o){let w=await Le(i,u.id,e);if(!w)return {success:false,error:new l("IDENTIFIER_NOT_FOUND",`Identifier not found: ${u.id}`)};if(w.value!==u.value&&await se(i,u.value))return {success:false,error:new l("IDENTIFIER_ALREADY_EXISTS",`Identifier already taken: ${u.value}`)}}return await i.transaction().execute(async u=>{for(let w of o)await Rr(u,w.id,e,{type:w.type,value:w.value});}),{success:true,identifiers:(await ne(i,e)).map(u=>({id:u.id,type:u.type,value:u.value}))}}n(Ee,"bulkUpdateIdentifiers");async function Te(e,r,t){let s=D(t.dialect);if(r.length===0)return {success:false,error:new l("VALIDATION_ERROR","At least one ID is required")};let i=Array.from(new Set(r));for(let c of i)if(!await Le(s,c,e))return {success:false,error:new l("IDENTIFIER_NOT_FOUND",`Identifier not found: ${c}`)};return await Ir(s,e)-i.length<1?{success:false,error:new l("VALIDATION_ERROR","Cannot delete all identifiers \u2014 at least one must remain")}:(await _r(s,e,i),{success:true,identifiers:(await ne(s,e)).map(c=>({id:c.id,type:c.type,value:c.value}))})}n(Te,"bulkDeleteIdentifiers");async function Tr(e,r){let t=D(r.dialect);return (await kr(t,e)).map(i=>({id:i.id,ipAddress:i.ipAddress,userAgent:i.userAgent,createdAt:i.createdAt,expiresAt:i.expiresAt}))}n(Tr,"getSessions");async function xr(e,r,t){let s=D(t.dialect),i=await me(s,r);i&&i.userId===e&&await pe(s,r);}n(xr,"revokeSession");function K(e){return k(e).cookieName}n(K,"getCookieName");function xe(e){return k(e).accessCookieName}n(xe,"getAccessCookieName");function H(e,r){if(!e)return;let t=`${r}=`,s=0;for(;s<e.length;){for(;s<e.length&&e[s]===" ";)s++;let i=e.indexOf(";",s),o=i===-1?e.length:i;if(e.startsWith(t,s))return e.slice(s+t.length,o);s=o+1;}}n(H,"readCookie");function ie(e,r,t){let s=t.cookie??{},i=V(k(t).refreshExpiresIn);e.cookie(K(t),r,{httpOnly:s.httpOnly??true,secure:s.secure??false,sameSite:s.sameSite??"strict",path:s.path??"/",maxAge:i});}n(ie,"setCookieFromConfig");function Ne(e,r){let t=r.cookie??{};e.clearCookie(K(r),{path:t.path??"/"});}n(Ne,"clearCookieFromConfig");function oe(e,r,t){if(!t.accessCookie)return;let s=t.accessCookie,i=V(k(t).accessExpiresIn);e.cookie(xe(t),r,{httpOnly:false,secure:s.secure??false,sameSite:s.sameSite??"strict",path:s.path??"/",maxAge:i});}n(oe,"setAccessCookieFromConfig");function Pe(e,r){if(!r.accessCookie)return;let t=r.accessCookie;e.clearCookie(xe(r),{path:t.path??"/"});}n(Pe,"clearAccessCookieFromConfig");function De(e,r){let t=e.headers.authorization;return t?.startsWith("Bearer ")?t.slice(7):H(e.headers.cookie,xe(r))}n(De,"getCurrentAccessToken");function O(e){let r=e.logger??L,t=e.loggerService??"sentri";return e.mode==="client"?Gr(e.keyUri,r,t):Wr(e,r,t)}n(O,"protect");function Gr(e,r,t){return async(s,i,o)=>{let f=s.headers.authorization,c=f?.startsWith("Bearer ")?f.slice(7):void 0,a=s.requestId;if(!c)return r.warn(p(t,"auth.protect.failure",{mode:"client",errorCode:"UNAUTHORIZED",...a!==void 0&&{requestId:a}})),o(new l("UNAUTHORIZED","Missing or malformed Authorization header"));try{let u=await nr(e),w=mr(c,u);s.user={id:w.id,roles:w.roles},r.info(p(t,"auth.protect.success",{mode:"client",userId:w.id,...a!==void 0&&{requestId:a}})),o();}catch(u){let w=u instanceof l?u.code:"TOKEN_INVALID";r.warn(p(t,"auth.protect.failure",{mode:"client",errorCode:w,...a!==void 0&&{requestId:a}})),o(u);}}}n(Gr,"protectClient");function Wr(e,r,t){return async(s,i,o)=>{let f=De(s,e),c=s.requestId;if(!f)return r.warn(p(t,"auth.protect.failure",{mode:"server",errorCode:"UNAUTHORIZED",...c!==void 0&&{requestId:c}})),o(new l("UNAUTHORIZED","Missing or malformed Authorization header"));try{let a=fe(f,e);if(e.isTokenRevoked&&await e.isTokenRevoked(a.sessionId))return r.warn(p(t,"auth.protect.token_revoked",{mode:"server",userId:a.id,...c!==void 0&&{requestId:c}})),o(new l("UNAUTHORIZED","Token has been revoked"));s.user={id:a.id,roles:a.roles},r.info(p(t,"auth.protect.success",{mode:"server",userId:a.id,...c!==void 0&&{requestId:c}})),o();}catch(a){if(a instanceof l&&a.code==="TOKEN_EXPIRED"){let u=H(s.headers.cookie,K(e));if(!u)return r.warn(p(t,"auth.protect.failure",{mode:"server",errorCode:"TOKEN_EXPIRED",...c!==void 0&&{requestId:c}})),o(new l("UNAUTHORIZED","Token expired. Please login again."));try{let w=await B(u,e);if(!w.success)return r.warn(p(t,"auth.protect.failure",{mode:"server",errorCode:w.error.code,...c!==void 0&&{requestId:c}})),o(new l("UNAUTHORIZED","Session expired. Please login again."));ie(i,w.refreshToken,e),oe(i,w.accessToken,e),i.setHeader("X-New-Access-Token",w.accessToken),s.user=w.user,r.info(p(t,"auth.protect.auto_refresh",{mode:"server",userId:w.user.id,...c!==void 0&&{requestId:c}})),o();}catch{r.warn(p(t,"auth.protect.failure",{mode:"server",errorCode:"TOKEN_EXPIRED",...c!==void 0&&{requestId:c}})),o(new l("UNAUTHORIZED","Session expired. Please login again."));}}else {let u=a instanceof l?a.code:"TOKEN_INVALID";r.warn(p(t,"auth.protect.failure",{mode:"server",errorCode:u,...c!==void 0&&{requestId:c}})),o(a);}}}}n(Wr,"protectServer");function Nr(e,r,t){let s=`Requires one of roles: ${e.join(", ")}`;return (i,o,f)=>{let c=i.requestId;if(!i.user)return r.warn(p(t,"auth.authorize.unauthenticated",{requiredRoles:e,...c!==void 0&&{requestId:c}})),f(new l("UNAUTHORIZED","Not authenticated"));let a=i.user.roles;if(!e.some(u=>a.includes(u)))return r.warn(p(t,"auth.authorize.denied",{userId:i.user.id,userRoles:[...a],requiredRoles:e,...c!==void 0&&{requestId:c}})),f(new l("FORBIDDEN",s));r.info(p(t,"auth.authorize.passed",{userId:i.user.id,userRoles:[...a],requiredRoles:e,...c!==void 0&&{requestId:c}})),f();}}n(Nr,"createAuthorizeHandler");function ae(e,r){return n(function(...s){return Nr(s,e,r)},"authorize")}n(ae,"createAuthorize");function Zr(...e){return Nr(e,L,"sentri")}n(Zr,"authorize");var Yr=new l("FORBIDDEN","You do not have permission to perform this action");function Dr(e,r,t){return async(s,i,o)=>{let f=s.requestId;if(!s.user)return r.warn(p(t,"auth.permit.unauthenticated",{...f!==void 0&&{requestId:f}})),o(new l("UNAUTHORIZED","Not authenticated"));let c=s.user.id;if(e.roles&&e.roles.length>0){let a=s.user.roles;if(e.roles.some(u=>a.includes(u)))return r.info(p(t,"auth.permit.role_bypass",{userId:c,bypassedByRole:true,...f!==void 0&&{requestId:f}})),o()}try{let a=e.check(s);(a instanceof Promise?await a:a)?(r.info(p(t,"auth.permit.passed",{userId:c,...f!==void 0&&{requestId:f}})),o()):(r.warn(p(t,"auth.permit.denied",{userId:c,...f!==void 0&&{requestId:f}})),o(Yr));}catch(a){o(a);}}}n(Dr,"createPermitHandler");function de(e,r){return n(function(s){return Dr(typeof s=="function"?{check:s}:s,e,r)},"permit")}n(de,"createPermit");function qr(e){return Dr(typeof e=="function"?{check:e}:e,L,"sentri")}n(qr,"permit");function ue(e){return (r,t,s,i)=>{if(r instanceof l){s.status(r.statusCode).json({error:true,statusCode:r.statusCode,code:r.code,message:r.message,data:null});return}e?.onUnhandled?.(r),s.status(500).json({error:true,statusCode:500,code:"INTERNAL_SERVER_ERROR",message:"Internal server error",data:null});}}n(ue,"createErrorHandler");var Sr=new Map;function Cr(e){let r=Sr.get(e);return r||(r=new Redis(e,{lazyConnect:true,enableOfflineQueue:false}),r.on("error",t=>{console.error("Sentri Redis Client Error:",t.message);}),Sr.set(e,r)),r}n(Cr,"getRedisClient");function Or(e){let r=e?.ttl??3e5,t=(e?.header??"X-Idempotency-Key").toLowerCase(),s=new Set((e?.methods??["POST","PUT","PATCH"]).map(o=>o.toUpperCase())),i=e?.redisUrl;return i?et(i,r,t,s):rt(r,t,s,e?.maxSize??1e4)}n(Or,"createIdempotencyMiddleware");function et(e,r,t,s){let i=Cr(e),o="sentri:idempotency:";return async(f,c,a)=>{let u=f.headers[t];if(!u||typeof u!="string"||!s.has(f.method))return a();f.requestId=u,c.setHeader("X-Request-Id",u);let w=await i.get(`${o}${u}`);if(w){let T=JSON.parse(w);return c.setHeader("X-Idempotent-Replayed","true"),c.status(T.statusCode).json(T.body)}let C=c.json.bind(c);c.json=n(function(S){if(c.statusCode>=200&&c.statusCode<300){let A={statusCode:c.statusCode,body:S,expiresAt:Date.now()+r};i.set(`${o}${u}`,JSON.stringify(A),"PX",r).catch(()=>{});}return C(S)},"idempotentJson"),a();}}n(et,"buildRedisMiddleware");function rt(e,r,t,s){let i=Math.max(e,5e3),o=new Map,f=setInterval(()=>{let c=Date.now();for(let[a,u]of o)u.expiresAt<=c&&o.delete(a);},i);return typeof f=="object"&&f!==null&&"unref"in f&&f.unref(),(c,a,u)=>{let w=c.headers[r];if(!w||typeof w!="string"||!t.has(c.method))return u();c.requestId=w,a.setHeader("X-Request-Id",w);let C=Date.now(),T=o.get(w);if(T&&T.expiresAt>C)return a.setHeader("X-Idempotent-Replayed","true"),a.status(T.statusCode).json(T.body);let S=a.json.bind(a);a.json=n(function(P){if(a.statusCode>=200&&a.statusCode<300){if(o.size>=s){let $=o.keys().next().value;$!==void 0&&o.delete($);}o.set(w,{statusCode:a.statusCode,body:P,expiresAt:Date.now()+e});}return S(P)},"idempotentJson"),u();}}n(rt,"buildMemoryMiddleware");var Ke=class{static{n(this,"InMemoryRateLimiter");}store=new Map;constructor(){setInterval(()=>{let r=Date.now();for(let[t,s]of this.store.entries())s.expiresAt<r&&this.store.delete(t);},6e4).unref();}async consume(r,t,s){let i=Date.now(),o=this.store.get(r);if((!o||o.expiresAt<i)&&(o={count:0,expiresAt:i+s},this.store.set(r,o)),o.count>t){let f=Math.ceil((o.expiresAt-i)/1e3);throw new Error(`Rate limit exceeded. Try again in ${f} seconds.`)}o.count++;}},$e=class{static{n(this,"RedisRateLimiter");}redis;logger;constructor(r,t){this.redis=r,this.logger=t;}async consume(r,t,s){let i=`sentri:rl:${r}`,o=Math.ceil(s/1e3);try{let f=await this.redis.multi().incr(i).expire(i,o,"NX").exec();if(!f||f.length===0)return;if(f[0][1]>t)throw new Error("Rate limit exceeded. Try again later.")}catch(f){if(f instanceof Error&&f.message.includes("Rate limit exceeded"))throw f;this.logger.error({msg:"Redis RateLimiter failed, bypassing limit",error:f});}}},X=null;async function br(e,r){if(X)return X;if(e)try{let{Redis:t}=await import('ioredis'),s=new t(e,{lazyConnect:!0,maxRetriesPerRequest:1});return await s.connect(),r?.info({msg:"Connected to Redis for Rate Limiting"}),X=new $e(s,r??L),X}catch(t){r?.warn({msg:"Failed to connect to Redis for Rate Limiting, falling back to InMemoryRateLimiter",error:t});}return X=new Ke,X}n(br,"getRateLimiter");var Se=8,z=72,Ce=255,Ur=100,J=50;function g(e){return new l("VALIDATION_ERROR",e)}n(g,"badRequest");function b(e,r,t,s){e.status(r).json({error:false,statusCode:r,message:t,data:s});}n(b,"ok");function F(e,r){e.status(r.statusCode).json({error:true,statusCode:r.statusCode,code:r.code,message:r.message,data:null});}n(F,"fail");function M(e){if(e==null||typeof e!="object"||Array.isArray(e))throw new l("VALIDATION_ERROR","Request body is missing or not a JSON object. Did you apply express.json()?");return e}n(M,"parseBody");function st(e,r){if(!r.apiKey)return;let t=e.headers["x-api-key"];if(typeof t!="string"||t!==r.apiKey)throw new l("UNAUTHORIZED","Invalid or missing API key")}n(st,"validateApiKey");function He(e){if(e)try{let r=e();r instanceof Promise&&r.catch(()=>{});}catch{}}n(He,"fireHook");function nt(e){return e.startsWith("RS")||e.startsWith("PS")}n(nt,"isRSA");function Me(e,r){if(typeof e!="object"||e===null||Array.isArray(e))throw g(`identifiers[${r}] must be an object`);let t=e;if(typeof t.type!="string"||t.type.trim().length===0)throw g(`identifiers[${r}].type is required and must be a non-empty string`);if(t.type.length>Ur)throw g(`identifiers[${r}].type must not exceed ${Ur} characters`);if(typeof t.value!="string"||t.value.trim().length===0)throw g(`identifiers[${r}].value is required and must be a non-empty string`);if(t.value.length>Ce)throw g(`identifiers[${r}].value must not exceed ${Ce} characters`);return {type:t.type,value:t.value}}n(Me,"validateIdentifierInput");function E(e){return e.requestId!==void 0?{requestId:e.requestId}:{}}n(E,"reqId");function Lr(e){let r=Router(),t=e,s=k(t),i=e.logger??L,o=e.loggerService??"sentri",f=ae(i,o),c=de(i,o),a=br(e.redisUrl,i),u=e.router?.register??(d=>ye(d,t)),w=e.router?.login??(d=>Ie(d,t)),C=e.router?.refresh??(d=>B(d,t)),T=e.router?.logout??(d=>d!==void 0?Re(d,t):Promise.resolve()),S=e.router?.logoutAll??(d=>_e(d,t)),A=e.router?.getUser??(d=>ge(d,t)),P=e.router?.assignRoles??((d,m)=>Ae(d,m,t)),$=e.router?.changePassword??((d,m,v)=>ve(d,m,v,t)),ce=e.router?.bulkCreateIdentifiers??((d,m)=>ke(d,m,t)),Fr=e.router?.bulkUpdateIdentifiers??((d,m)=>Ee(d,m,t)),Pr=e.router?.bulkDeleteIdentifiers??((d,m)=>Te(d,m,t));nt(s.algorithm)&&r.get("/keys",(d,m)=>{m.setHeader("Cache-Control","public, max-age=3600"),m.json(tr(e.secret));}),r.post("/register",async(d,m,v)=>{let R=Date.now();try{st(d,e);let h=M(d.body),{identifiers:I,password:_,roles:y}=h;if(!Array.isArray(I)||I.length===0)throw g("identifiers is required and must be a non-empty array");if(I.length>J)throw g(`identifiers must not exceed ${J} entries`);let x=I.map((Z,Oe)=>Me(Z,Oe));if(typeof _!="string"||_.length<Se)throw g(`password is required and must be at least ${Se} characters`);if(_.length>z)throw g(`password must not exceed ${z} characters`);if(y!==void 0&&!Array.isArray(y))throw g("roles must be an array of strings when provided");if(Array.isArray(y)&&!y.every(Z=>typeof Z=="string"))throw g("each role must be a string");let U=Array.isArray(y)?y:void 0,N=U!==void 0?{identifiers:x,password:_,roles:U}:{identifiers:x,password:_},j=d.ip||"127.0.0.1",G=d.get("user-agent")||"Unknown";if(e.rateLimit!==!1){let Z=await a,Oe=typeof e.rateLimit=="object"?e.rateLimit.maxRegisterAttempts??5:5;await Z.consume(`register:${j}`,Oe,900*1e3);}let W=await u(N,{ip:j,userAgent:G});if(!W.success){i.warn(p(o,"auth.register.failure",{errorCode:W.error.code,duration_ms:Date.now()-R,...E(d)})),F(m,W.error);return}i.info(p(o,"auth.register.success",{userId:W.user.id,duration_ms:Date.now()-R,...E(d)})),b(m,201,"User registered successfully",{user:W.user});}catch(h){v(h);}}),r.post("/login",async(d,m,v)=>{let R=Date.now();try{let h=M(d.body),{identifier:I,password:_}=h;if(typeof I!="string"||I.trim().length===0)throw g("identifier is required and must be a non-empty string");if(I.length>Ce)throw g(`identifier must not exceed ${Ce} characters`);if(typeof _!="string"||_.length===0)throw g("password is required");if(_.length>z)throw g(`password must not exceed ${z} characters`);let y=I.trim(),x=d.ip||"127.0.0.1",U=d.get("user-agent")||"Unknown";if(e.rateLimit!==!1){let j=await a,G=typeof e.rateLimit=="object"?e.rateLimit.maxLoginAttempts??5:5;await j.consume(`login:${x}`,G,900*1e3);}let N=await w({identifier:y,password:_},{ip:x,userAgent:U});if(!N.success){He(()=>e.hooks?.onLoginFailed?.(y,N.error,{ip:x})),i.warn(p(o,"auth.login.failure",{errorCode:N.error.code,duration_ms:Date.now()-R,...E(d)})),F(m,N.error);return}He(()=>e.hooks?.onLoginSuccess?.(N.user,{ip:x,userAgent:U})),ie(m,N.refreshToken,e),oe(m,N.accessToken,e),i.info(p(o,"auth.login.success",{userId:N.user.id,duration_ms:Date.now()-R,...E(d)})),b(m,200,"Login successful",{accessToken:N.accessToken,user:N.user});}catch(h){v(h);}}),r.post("/refresh",async(d,m,v)=>{let R=Date.now();try{let h=H(d.headers.cookie,K(e));if(!h)throw new l("UNAUTHORIZED","Refresh token cookie is missing");let I=d.ip||"127.0.0.1",_=d.get("user-agent")||"Unknown",y=await C(h,{ip:I,userAgent:_});if(!y.success){Ne(m,e),i.warn(p(o,"auth.refresh.failure",{errorCode:y.error.code,duration_ms:Date.now()-R,...E(d)})),F(m,y.error);return}ie(m,y.refreshToken,e),oe(m,y.accessToken,e),i.info(p(o,"auth.refresh.success",{userId:y.user.id,duration_ms:Date.now()-R,...E(d)})),b(m,200,"Token refreshed",{accessToken:y.accessToken});}catch(h){v(h);}}),r.post("/logout",async(d,m,v)=>{let R=Date.now();try{let h=H(d.headers.cookie,K(e));await T(h),Ne(m,e),Pe(m,e),i.info(p(o,"auth.logout",{duration_ms:Date.now()-R,...E(d)})),b(m,200,"Logged out",null);}catch(h){v(h);}}),r.post("/logout-all",O(e),async(d,m,v)=>{let R=Date.now();try{let h=d.user.id;await S(h),He(()=>e.hooks?.onLogout?.(h)),Ne(m,e),Pe(m,e),i.info(p(o,"auth.logout_all",{userId:h,duration_ms:Date.now()-R,...E(d)})),b(m,200,"All sessions revoked",null);}catch(h){v(h);}}),r.get("/me",O(e),async(d,m,v)=>{let R=Date.now();try{let h=await A(d.user.id);if(!h.success){i.warn(p(o,"auth.me.failure",{userId:d.user.id,errorCode:h.error.code,duration_ms:Date.now()-R,...E(d)})),F(m,h.error);return}i.info(p(o,"auth.me.success",{userId:h.user.id,duration_ms:Date.now()-R,...E(d)})),b(m,200,"OK",h.user);}catch(h){v(h);}}),r.get("/me/identifiers",O(e),async(d,m,v)=>{let R=Date.now();try{let h=await A(d.user.id);if(!h.success){i.warn(p(o,"auth.me.identifiers.failure",{userId:d.user.id,errorCode:h.error.code,duration_ms:Date.now()-R,...E(d)})),F(m,h.error);return}i.info(p(o,"auth.me.identifiers.success",{userId:h.user.id,count:h.user.identifiers?.length??0,duration_ms:Date.now()-R,...E(d)})),b(m,200,"OK",{identifiers:h.user.identifiers??[]});}catch(h){v(h);}});let le=c(d=>!!d.user);return r.post("/me/identifiers",O(e),le,async(d,m,v)=>{let R=Date.now();try{let h=M(d.body),{identifiers:I}=h;if(!Array.isArray(I)||I.length===0)throw g("identifiers is required and must be a non-empty array");if(I.length>J)throw g(`identifiers must not exceed ${J} entries`);let _=I.map((x,U)=>Me(x,U)),y=await ce(d.user.id,_);if(!y.success){i.warn(p(o,"auth.identifiers.create_failure",{userId:d.user.id,errorCode:y.error.code,duration_ms:Date.now()-R,...E(d)})),F(m,y.error);return}i.info(p(o,"auth.identifiers.created",{userId:d.user.id,count:y.identifiers.length,duration_ms:Date.now()-R,...E(d)})),b(m,201,"Identifiers added successfully",{identifiers:y.identifiers});}catch(h){v(h);}}),r.put("/me/identifiers",O(e),le,async(d,m,v)=>{let R=Date.now();try{let h=M(d.body),{identifiers:I}=h;if(!Array.isArray(I)||I.length===0)throw g("identifiers is required and must be a non-empty array");if(I.length>J)throw g(`identifiers must not exceed ${J} entries`);let _=I.map((x,U)=>{if(typeof x!="object"||x===null||Array.isArray(x))throw g(`identifiers[${U}] must be an object`);let N=x;if(typeof N.id!="string"||N.id.trim().length===0)throw g(`identifiers[${U}].id is required and must be a non-empty string`);let{type:j,value:G}=Me(x,U);return {id:N.id,type:j,value:G}}),y=await Fr(d.user.id,_);if(!y.success){i.warn(p(o,"auth.identifiers.update_failure",{userId:d.user.id,errorCode:y.error.code,duration_ms:Date.now()-R,...E(d)})),F(m,y.error);return}i.info(p(o,"auth.identifiers.updated",{userId:d.user.id,count:y.identifiers.length,duration_ms:Date.now()-R,...E(d)})),b(m,200,"Identifiers updated successfully",{identifiers:y.identifiers});}catch(h){v(h);}}),r.delete("/me/identifiers",O(e),le,async(d,m,v)=>{let R=Date.now();try{let h=M(d.body),{ids:I}=h;if(!Array.isArray(I)||I.length===0)throw g("ids is required and must be a non-empty array of strings");if(!I.every(y=>typeof y=="string"))throw g("each id must be a string");let _=await Pr(d.user.id,I);if(!_.success){i.warn(p(o,"auth.identifiers.delete_failure",{userId:d.user.id,errorCode:_.error.code,duration_ms:Date.now()-R,...E(d)})),F(m,_.error);return}i.info(p(o,"auth.identifiers.deleted",{userId:d.user.id,duration_ms:Date.now()-R,...E(d)})),b(m,200,"Identifiers deleted successfully",{identifiers:_.identifiers});}catch(h){v(h);}}),r.patch("/me/password",O(e),le,async(d,m,v)=>{let R=Date.now();try{let h=M(d.body),{currentPassword:I,newPassword:_}=h;if(typeof I!="string"||I.length===0)throw g("currentPassword is required");if(typeof _!="string"||_.length<Se)throw g(`newPassword must be at least ${Se} characters`);if(_.length>z)throw g(`newPassword must not exceed ${z} characters`);if(I===_)throw g("newPassword must be different from currentPassword");let y=await $(d.user.id,I,_);if(!y.success){i.warn(p(o,"auth.password.change_failure",{userId:d.user.id,errorCode:y.error.code,duration_ms:Date.now()-R,...E(d)})),F(m,y.error);return}i.info(p(o,"auth.password.changed",{userId:d.user.id,duration_ms:Date.now()-R,...E(d)})),b(m,200,"Password updated successfully. All sessions have been revoked.",null);}catch(h){v(h);}}),r.post("/users/:userId/roles",O(e),f("admin"),async(d,m,v)=>{let R=Date.now();try{let h=M(d.body),{roles:I}=h,_=d.params.userId,y=typeof _=="string"?_:void 0;if(!y)throw g("userId is required");if(!Array.isArray(I)||I.length===0)throw g("roles must be a non-empty array of strings");if(!I.every(U=>typeof U=="string"))throw g("each role must be a string");let x=await P(y,I);if(!x.success){i.warn(p(o,"auth.roles.assign_failure",{targetUserId:y,errorCode:x.error.code,duration_ms:Date.now()-R,...E(d)})),F(m,x.error);return}i.info(p(o,"auth.roles.assigned",{targetUserId:y,roles:I,duration_ms:Date.now()-R,...E(d)})),b(m,200,"Roles assigned successfully",{user:x.user});}catch(h){v(h);}}),r.use(ue()),r.get("/sessions",O(e),async(d,m,v)=>{let R=Date.now();try{let h=d.user.id,_=await(e.router?.getSessions??(y=>Tr(y,t)))(h);i.info(p(o,"auth.sessions.get",{userId:h,duration_ms:Date.now()-R,...E(d)})),b(m,200,"OK",{sessions:_});}catch(h){v(h);}}),r.delete("/sessions/:id",O(e),async(d,m,v)=>{let R=Date.now();try{let h=d.user.id,I=d.params.id;await(e.router?.revokeSession??((y,x)=>xr(y,x,t)))(h,I),i.info(p(o,"auth.sessions.revoke",{userId:h,sessionId:I,duration_ms:Date.now()-R,...E(d)})),b(m,200,"Session revoked",null);}catch(h){v(h);}}),r}n(Lr,"createAuthRouter");function tn(e){if(e.mode==="client"){let a={mode:"client",keyUri:e.keyUri,...e.validRoles!==void 0&&{validRoles:e.validRoles},...e.logger!==void 0&&{logger:e.logger},...e.loggerService!==void 0&&{loggerService:e.loggerService}};Ge(a);let u=a.logger??L,w=a.loggerService??"sentri",C=ae(u,w),T=de(u,w);return {protect:n(()=>O(a),"protect"),authorize:n((...S)=>C(...S),"authorize"),permit:n(S=>T(S),"permit"),errorHandler:n(S=>ue(S),"errorHandler")}}let{privateKey:r}=generateKeyPairSync("rsa",{modulusLength:2048,privateKeyEncoding:{type:"pkcs8",format:"pem"},publicKeyEncoding:{type:"spki",format:"pem"}}),t={mode:"server",dialect:e.dialect,secret:r,algorithm:"RS256",validRoles:e.validRoles,...e.validIdentifiers!==void 0&&{validIdentifiers:e.validIdentifiers},...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},...e.logger!==void 0&&{logger:e.logger},...e.loggerService!==void 0&&{loggerService:e.loggerService}},s=t.logger??L,i=t.loggerService??"sentri",o=k(t),f=ae(s,i),c=de(s,i);return {protect:n(()=>O(t),"protect"),authorize:n((...a)=>f(...a),"authorize"),permit:n(a=>c(a),"permit"),errorHandler:n(a=>ue(a),"errorHandler"),hashPassword:n(a=>Y(a,o.saltRounds),"hashPassword"),verifyPassword:n((a,u)=>q(a,u),"verifyPassword"),signAccessToken:n(a=>ee(a,t),"signAccessToken"),signRefreshToken:n(a=>re(a,t),"signRefreshToken"),verifyAccessToken:n(a=>fe(a,t),"verifyAccessToken"),verifyRefreshToken:n(a=>te(a,t),"verifyRefreshToken"),getCurrentAccessToken:n(a=>De(a,t),"getCurrentAccessToken"),router:n(()=>Lr(t),"router"),migrate:n(()=>We(D(t.dialect)),"migrate"),idempotencyMiddleware:n(a=>Or({...a,...t.redisUrl!==void 0&&{redisUrl:t.redisUrl}}),"idempotencyMiddleware"),register:n(a=>ye(a,t),"register"),login:n(a=>Ie(a,t),"login"),refresh:n(a=>B(a,t),"refresh"),logout:n(a=>Re(a,t),"logout"),logoutAll:n(a=>_e(a,t),"logoutAll"),getUser:n(a=>ge(a,t),"getUser"),changePassword:n((a,u,w)=>ve(a,u,w,t),"changePassword"),assignRoles:n((a,u)=>Ae(a,u,t),"assignRoles"),bulkCreateIdentifiers:n((a,u)=>ke(a,u,t),"bulkCreateIdentifiers"),bulkUpdateIdentifiers:n((a,u)=>Ee(a,u,t),"bulkUpdateIdentifiers"),bulkDeleteIdentifiers:n((a,u)=>Te(a,u,t),"bulkDeleteIdentifiers")}}n(tn,"createAuthExpress");export{je as SENTRI_ERROR_STATUS,l as SentriError,Zr as authorize,tn as createAuthExpress,Lr as createAuthRouter,ae as createAuthorize,ue as createErrorHandler,Or as createIdempotencyMiddleware,de as createPermit,De as getCurrentAccessToken,qr as permit,O as protect};
@@ -0,0 +1 @@
1
+ 'use strict';var crypto=require('crypto'),kysely=require('kysely'),Ze=require('bcrypt'),J=require('jsonwebtoken');require('@fastify/cookie');var stream=require('stream');function _interopDefault(e){return e&&e.__esModule?e:{default:e}}var Ze__default=/*#__PURE__*/_interopDefault(Ze);var J__default=/*#__PURE__*/_interopDefault(J);var Lr=Object.defineProperty;var n=(e,r)=>Lr(e,"name",{value:r,configurable:true});var $e=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,TOKEN_REUSE:401}),u=class extends Error{static{n(this,"SentriError");}code;statusCode;constructor(r,t,s){super(t),this.name="SentriError",this.code=r,this.statusCode=s??$e[r]??500;}};var Ve=new WeakMap,Me=32,Be=10,je=31;function Xe(e){if(e.mode==="client"){if(!e.keyUri||e.keyUri.trim().length===0)throw new u("CONFIGURATION_ERROR","keyUri must not be empty");return}if(!e.secret||e.secret.trim().length===0)throw new u("CONFIGURATION_ERROR","secret must not be empty");if((e.algorithm??"HS256").startsWith("HS")&&e.secret.length<Me)throw new u("CONFIGURATION_ERROR",`secret must be at least ${Me} characters for HMAC algorithms`);let s=e.saltRounds??12;if(!Number.isInteger(s)||s<Be||s>je)throw new u("CONFIGURATION_ERROR",`saltRounds must be an integer between ${Be} and ${je}`);if(!e.validRoles||e.validRoles.length===0)throw new u("CONFIGURATION_ERROR","validRoles must contain at least one role");if(e.validIdentifiers!==void 0&&e.validIdentifiers.length===0)throw new u("CONFIGURATION_ERROR","validIdentifiers must contain at least one identifier type");if(!e.dialect)throw new u("CONFIGURATION_ERROR","dialect is required in server mode")}n(Xe,"validateConfig");function g(e){let r=Ve.get(e);if(r)return r;let t={algorithm:e.algorithm??"HS256",accessExpiresIn:e.accessExpiresIn??"15m",refreshExpiresIn:e.refreshExpiresIn??"7d",saltRounds:e.saltRounds??12,validRoles:e.validRoles,validRolesSet:new Set(e.validRoles),validIdentifiers:e.validIdentifiers??["email","username"],validIdentifiersSet:new Set(e.validIdentifiers??["email","username"]),cookieName:e.cookie?.name??"refresh_token",accessCookieName:e.accessCookie?.name??"access_token"};return Ve.set(e,t),t}n(g,"resolveServerConfig");var Fr=/^(\d+)([smhdw])$/,Pr={s:1e3,m:6e4,h:36e5,d:864e5,w:6048e5},ze=new Map;function H(e){if(typeof e=="number")return e*1e3;let r=ze.get(e);if(r!==void 0)return r;let t=Fr.exec(e);if(!t?.[1]||!t?.[2])throw new Error(`Invalid expiresIn: "${e}". Use e.g. "15m", "7d", "1h".`);let s=Pr[t[2]];if(s===void 0)throw new Error(`Invalid expiresIn: "${e}". Use e.g. "15m", "7d", "1h".`);let i=parseInt(t[1],10)*s;return ze.set(e,i),i}n(H,"parseExpiry");var D={info:n(()=>{},"info"),warn:n(()=>{},"warn"),error:n(()=>{},"error")};function m(e,r,t){return {service:e,event:r,...t}}n(m,"buildLogData");async function Ge(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(kysely.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("ip_address","varchar(45)").addColumn("user_agent","text").addColumn("replaced_by","varchar(36)").addColumn("created_at","timestamp",r=>r.notNull().defaultTo(kysely.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("created_at","timestamp",r=>r.notNull().defaultTo(kysely.sql`CURRENT_TIMESTAMP`)).execute(),await e.schema.createIndex("idx_sentri_sessions_user_id").ifNotExists().on("sentri_sessions").column("user_id").execute(),await e.schema.createIndex("idx_sentri_identifiers_user_id").ifNotExists().on("sentri_identifiers").column("user_id").execute();}n(Ge,"runMigrations");var We=new Map;function T(e){let r=We.get(e);return r||(r=new kysely.Kysely({dialect:e}),We.set(e,r)),r}n(T,"getDatabase");async function W(e,r=12){return Ze__default.default.hash(e,r)}n(W,"hashPassword");async function Z(e,r){return Ze__default.default.compare(e,r)}n(Z,"verifyPassword");var Je=new Map,Ye=new Map,Vr=3600*1e3;function Qe(e){let r=Je.get(e);if(!r){let t=crypto.createPrivateKey(e),s=crypto.createPublicKey(t),i=s.export({format:"jwk"}),c=crypto.createHash("sha256").update(JSON.stringify({e:i.e,kty:i.kty,n:i.n})).digest("base64url"),y={...i,use:"sig",kid:c};r={kid:c,publicKey:s,jwk:y},Je.set(e,r);}return r}n(Qe,"getOrBuildKey");function er(e){let{jwk:r}=Qe(e);return {keys:[r]}}n(er,"buildJwks");function rr(e){return Qe(e).publicKey}n(rr,"getPublicKeyFromPrivate");async function tr(e){let r=Date.now(),t=Ye.get(e);if(t&&r-t.fetchedAt<Vr)return t.publicKey;let s=await fetch(e);if(!s.ok)throw new u("CONFIGURATION_ERROR",`Failed to fetch public key from ${e}: HTTP ${s.status}`);let i=await s.json();if(!i.keys||i.keys.length===0)throw new u("CONFIGURATION_ERROR",`No keys found in JWKS response from ${e}`);let o=i.keys[0],c=crypto.createPublicKey({key:o,format:"jwk"});return Ye.set(e,{publicKey:c,fetchedAt:r}),c}n(tr,"fetchPublicKey");var sr=new Map,nr=new Map,ir=new Map;function or(e){return e.startsWith("RS")||e.startsWith("PS")}n(or,"isRSA");function ar(e){let r=sr.get(e);return r||(r={access:`${e}:access`,refresh:`${e}:refresh`},sr.set(e,r)),r}n(ar,"getHsSecrets");function Br(e){let r=nr.get(e);return r||(r=crypto.createPrivateKey(e),nr.set(e,r)),r}n(Br,"getRsPrivateKey");function dr(e){let r=g(e);if(or(r.algorithm)){let i=Br(e.secret);return {accessKey:i,refreshKey:i}}let{access:t,refresh:s}=ar(e.secret);return {accessKey:t,refreshKey:s}}n(dr,"getSigningKeys");function cr(e,r){let t=g(e);if(or(t.algorithm))return rr(e.secret);let{access:s,refresh:i}=ar(e.secret);return r==="access"?s:i}n(cr,"getVerifyKey");function ur(e,r,t,s){let i=`${t}:${s}`,o=ir.get(i);return o||(o={expiresIn:t,algorithm:s},ir.set(i,o)),J__default.default.sign(e,r,o)}n(ur,"sign");function lr(e,r,t){try{let s=J__default.default.verify(e,r,{algorithms:[t]});if(typeof s=="string"||s===null)throw new u("TOKEN_INVALID","Token payload is not an object");return s}catch(s){throw s instanceof u?s:s instanceof J__default.default.TokenExpiredError?new u("TOKEN_EXPIRED","Token has expired"):new u("TOKEN_INVALID","Token is invalid or malformed")}}n(lr,"verify");function Y(e,r){let t=g(r),{accessKey:s}=dr(r);return ur(e,s,t.accessExpiresIn,t.algorithm)}n(Y,"signAccessToken");function q(e,r){let t=g(r),{refreshKey:s}=dr(r);return ur({sessionId:e},s,t.refreshExpiresIn,t.algorithm)}n(q,"signRefreshToken");function ce(e,r){let t=g(r),s=cr(r,"access");return lr(e,s,t.algorithm)}n(ce,"verifyAccessToken");function Q(e,r){let t=g(r),s=cr(r,"refresh");return lr(e,s,t.algorithm)}n(Q,"verifyRefreshToken");function fr(e,r){try{let t=J__default.default.verify(e,r,{algorithms:["RS256","RS384","RS512","PS256","PS384","PS512"]});if(typeof t=="string"||t===null)throw new u("TOKEN_INVALID","Token payload is not an object");return t}catch(t){throw t instanceof u?t:t instanceof J__default.default.TokenExpiredError?new u("TOKEN_EXPIRED","Token has expired"):new u("TOKEN_INVALID","Token is invalid or malformed")}}n(fr,"verifyTokenWithPublicKey");function Ce(e){try{return JSON.parse(e)}catch{return []}}n(Ce,"parseRoles");function jr(e){return JSON.stringify(e)}n(jr,"serializeRoles");function mr(e){return {id:e.id,userId:e.user_id,type:e.type,value:e.value,createdAt:new Date(e.created_at)}}n(mr,"mapIdentifier");async function ee(e,r){let t=await e.selectFrom("sentri_identifiers as i").innerJoin("sentri_users as u","u.id","i.user_id").select(["u.id","u.password_hash","u.roles"]).where("i.value","=",r).executeTakeFirst();return t?{id:t.id,passwordHash:t.password_hash,roles:Ce(t.roles)}:null}n(ee,"findUserByIdentifierValue");async function ue(e,r){let t=await e.selectFrom("sentri_users").select(["id","password_hash","roles"]).where("id","=",r).executeTakeFirst();return t?{id:t.id,passwordHash:t.password_hash,roles:Ce(t.roles)}:null}n(ue,"findUserById");async function pr(e,r,t){let s=t.map(i=>({id:crypto.randomUUID(),user_id:r,type:i.type,value:i.value}));return await e.insertInto("sentri_identifiers").values(s).execute(),s.map(i=>({id:i.id,userId:i.user_id,type:i.type,value:i.value,createdAt:new Date}))}n(pr,"createIdentifiers");async function re(e,r){return (await e.selectFrom("sentri_identifiers").selectAll().where("user_id","=",r).orderBy("created_at","asc").execute()).map(mr)}n(re,"findIdentifiersByUserId");async function Oe(e,r,t){let s=await e.selectFrom("sentri_identifiers").selectAll().where("id","=",r).where("user_id","=",t).executeTakeFirst();return s?mr(s):null}n(Oe,"findIdentifierById");async function wr(e,r){let t=await e.selectFrom("sentri_identifiers").select(s=>s.fn.countAll().as("count")).where("user_id","=",r).executeTakeFirst();return Number(t?.count??0)}n(wr,"countIdentifiersByUserId");async function Ir(e,r,t,s){await e.updateTable("sentri_identifiers").set({type:s.type,value:s.value}).where("id","=",r).where("user_id","=",t).execute();}n(Ir,"updateIdentifier");async function yr(e,r,t){await e.deleteFrom("sentri_identifiers").where("user_id","=",r).where("id","in",t).execute();}n(yr,"deleteIdentifiers");async function Rr(e,r,t){await e.updateTable("sentri_users").set({password_hash:t}).where("id","=",r).execute();}n(Rr,"updateUserPassword");async function _r(e,r,t){await e.updateTable("sentri_users").set({roles:jr(t)}).where("id","=",r).execute();}n(_r,"updateUserRoles");async function be(e,r){let t=crypto.randomUUID();return await e.insertInto("sentri_sessions").values({id:t,user_id:r.userId,expires_at:r.expiresAt.toISOString(),ip_address:r.ipAddress??null,user_agent:r.userAgent??null}).execute(),{id:t}}n(be,"createSession");async function le(e,r){let t=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.ip_address","s.user_agent","s.replaced_by","s.created_at as session_created_at","u.id as user_id_col","u.password_hash","u.roles"]).where("s.id","=",r).executeTakeFirst();return t?{id:t.session_id,userId:t.user_id,expiresAt:new Date(t.expires_at),ipAddress:t.ip_address,userAgent:t.user_agent,replacedBy:t.replaced_by,createdAt:new Date(t.session_created_at),user:{id:t.user_id_col,passwordHash:t.password_hash,roles:Ce(t.roles)}}:null}n(le,"findSessionById");async function fe(e,r){await e.deleteFrom("sentri_sessions").where("id","=",r).execute();}n(fe,"deleteSession");async function gr(e,r,t){await e.updateTable("sentri_sessions").set({replaced_by:t}).where("id","=",r).execute();}n(gr,"markSessionReplaced");async function he(e,r){await e.deleteFrom("sentri_sessions").where("user_id","=",r).execute();}n(he,"deleteAllSessionsForUser");async function vr(e,r){return (await e.selectFrom("sentri_sessions").selectAll().where("user_id","=",r).where("replaced_by","is",null).where("expires_at",">",new Date).execute()).map(s=>({id:s.id,userId:s.user_id,expiresAt:new Date(s.expires_at),ipAddress:s.ip_address,userAgent:s.user_agent,replacedBy:s.replaced_by,createdAt:new Date(s.created_at)}))}n(vr,"findSessionsByUserId");async function me(e,r,t){let s=g(r),i=T(r.dialect),o=e.roles??[],c=o.filter(A=>!s.validRolesSet.has(A));if(c.length>0)return {success:false,error:new u("INVALID_ROLE",`Invalid roles: ${c.join(", ")}`)};if(!e.identifiers||e.identifiers.length===0)return {success:false,error:new u("VALIDATION_ERROR","At least one identifier is required")};let y=e.identifiers.map(A=>({type:A.type.trim(),value:A.value.trim()})),d=y.filter(A=>!s.validIdentifiersSet.has(A.type));if(d.length>0)return {success:false,error:new u("VALIDATION_ERROR",`Invalid identifier types: ${d.map(A=>A.type).join(", ")}`)};if(new Set(y.map(A=>A.value)).size!==y.length)return {success:false,error:new u("VALIDATION_ERROR","Duplicate identifier values in request")};for(let A of y)if(await ee(i,A.value))return {success:false,error:new u("IDENTIFIER_ALREADY_EXISTS",`Identifier already taken: ${A.value}`)};let v=await W(e.password,s.saltRounds),{userId:C,identifierRows:x}=await i.transaction().execute(async A=>{let P=crypto.randomUUID();await A.insertInto("sentri_users").values({id:P,password_hash:v,roles:JSON.stringify(o)}).execute();let j=y.map(de=>({id:crypto.randomUUID(),user_id:P,type:de.type,value:de.value}));return await A.insertInto("sentri_identifiers").values(j).execute(),{userId:P,identifierRows:j}}),O={success:true,user:{id:C,roles:o,identifiers:x.map(A=>({id:A.id,type:A.type,value:A.value}))}};return r.hooks?.onRegister&&await r.hooks.onRegister(O.user),O}n(me,"register");async function pe(e,r,t){let s=g(r),i=T(r.dialect),o=await ee(i,e.identifier.trim());if(!o){let x=new u("INVALID_CREDENTIALS","Invalid credentials");return r.hooks?.onLoginFailed&&await r.hooks.onLoginFailed(e.identifier,x,{ip:t?.ip??""}),{success:false,error:x}}if(!await Z(e.password,o.passwordHash)){let x=new u("INVALID_CREDENTIALS","Invalid credentials");return r.hooks?.onLoginFailed&&await r.hooks.onLoginFailed(e.identifier,x,{ip:t?.ip??""}),{success:false,error:x}}let y=new Date(Date.now()+H(s.refreshExpiresIn)),d=await be(i,{userId:o.id,expiresAt:y,ipAddress:t?.ip??null,userAgent:t?.userAgent??null}),l={id:o.id,roles:o.roles},v=Y({id:o.id,roles:o.roles,sessionId:d.id},r),C=q(d.id,r);return r.hooks?.onLoginSuccess&&await r.hooks.onLoginSuccess(l,{ip:t?.ip??"",userAgent:t?.userAgent??""}),{success:true,accessToken:v,refreshToken:C,user:l}}n(pe,"login");async function $(e,r,t){let s=g(r),i=T(r.dialect),o;try{({sessionId:o}=Q(e,r));}catch(x){return x instanceof u?{success:false,error:x}:{success:false,error:new u("TOKEN_INVALID","Invalid refresh token")}}let c=await le(i,o);if(!c)return {success:false,error:new u("UNAUTHORIZED","Session not found or revoked")};if(c.replacedBy)return await he(i,c.userId),{success:false,error:new u("TOKEN_REUSE","Token reuse detected. All sessions revoked.")};if(c.expiresAt.getTime()<Date.now())return await fe(i,o),{success:false,error:new u("TOKEN_EXPIRED","Session has expired")};let y=new Date(Date.now()+H(s.refreshExpiresIn)),d=await be(i,{userId:c.userId,expiresAt:y,ipAddress:t?.ip??null,userAgent:t?.userAgent??null});await gr(i,o,d.id);let l={id:c.user.id,roles:c.user.roles},v=Y({...l,sessionId:d.id},r),C=q(d.id,r);return {success:true,accessToken:v,refreshToken:C,user:l}}n($,"refresh");async function we(e,r){let t=T(r.dialect),s;try{({sessionId:s}=Q(e,r));}catch{return}let i=await le(t,s);i&&(await fe(t,s),r.hooks?.onLogout&&await r.hooks.onLogout(i.userId));}n(we,"logout");async function Ie(e,r){let t=T(r.dialect);await he(t,e),r.hooks?.onLogout&&await r.hooks.onLogout(e);}n(Ie,"logoutAll");async function ye(e,r){let t=T(r.dialect),s=await ue(t,e);if(!s)return {success:false,error:new u("USER_NOT_FOUND","User not found")};let i=await re(t,e);return {success:true,user:{id:s.id,roles:s.roles,identifiers:i.map(o=>({id:o.id,type:o.type,value:o.value}))}}}n(ye,"getUser");async function Re(e,r,t,s){let i=g(s),o=T(s.dialect),c=await ue(o,e);if(!c)return {success:false,error:new u("USER_NOT_FOUND","User not found")};if(!await Z(r,c.passwordHash))return {success:false,error:new u("INVALID_CREDENTIALS","Invalid credentials")};let d=await W(t,i.saltRounds);return await Rr(o,e,d),await he(o,e),s.hooks?.onPasswordChanged&&await s.hooks.onPasswordChanged(e),{success:true}}n(Re,"changePassword");async function _e(e,r,t){let s=g(t),i=T(t.dialect),o=r.filter(l=>!s.validRolesSet.has(l));if(o.length>0)return {success:false,error:new u("INVALID_ROLE",`Invalid roles: ${o.join(", ")}`)};let c=await ue(i,e);if(!c)return {success:false,error:new u("USER_NOT_FOUND","User not found")};let y=new Set(c.roles);for(let l of r)y.add(l);let d=Array.from(y);return await _r(i,e,d),{success:true,user:{id:c.id,roles:d}}}n(_e,"assignRoles");async function ge(e,r,t){let s=g(t),i=T(t.dialect);if(r.length===0)return {success:false,error:new u("VALIDATION_ERROR","At least one identifier is required")};let o=r.map(l=>({type:l.type.trim(),value:l.value.trim()})),c=o.filter(l=>!s.validIdentifiersSet.has(l.type));if(c.length>0)return {success:false,error:new u("VALIDATION_ERROR",`Invalid identifier types: ${c.map(l=>l.type).join(", ")}`)};if(new Set(o.map(l=>l.value)).size!==o.length)return {success:false,error:new u("VALIDATION_ERROR","Duplicate identifier values in request")};for(let l of o)if(await ee(i,l.value))return {success:false,error:new u("IDENTIFIER_ALREADY_EXISTS",`Identifier already taken: ${l.value}`)};return await pr(i,e,o),{success:true,identifiers:(await re(i,e)).map(l=>({id:l.id,type:l.type,value:l.value}))}}n(ge,"bulkCreateIdentifiers");async function ve(e,r,t){let s=g(t),i=T(t.dialect);if(r.length===0)return {success:false,error:new u("VALIDATION_ERROR","At least one update is required")};let o=r.map(l=>({id:l.id,type:l.type.trim(),value:l.value.trim()})),c=o.filter(l=>!s.validIdentifiersSet.has(l.type));if(c.length>0)return {success:false,error:new u("VALIDATION_ERROR",`Invalid identifier types: ${c.map(l=>l.type).join(", ")}`)};if(new Set(o.map(l=>l.value)).size!==o.length)return {success:false,error:new u("VALIDATION_ERROR","Duplicate identifier values in request")};for(let l of o){let v=await Oe(i,l.id,e);if(!v)return {success:false,error:new u("IDENTIFIER_NOT_FOUND",`Identifier not found: ${l.id}`)};if(v.value!==l.value&&await ee(i,l.value))return {success:false,error:new u("IDENTIFIER_ALREADY_EXISTS",`Identifier already taken: ${l.value}`)}}return await i.transaction().execute(async l=>{for(let v of o)await Ir(l,v.id,e,{type:v.type,value:v.value});}),{success:true,identifiers:(await re(i,e)).map(l=>({id:l.id,type:l.type,value:l.value}))}}n(ve,"bulkUpdateIdentifiers");async function Ae(e,r,t){let s=T(t.dialect);if(r.length===0)return {success:false,error:new u("VALIDATION_ERROR","At least one ID is required")};let i=Array.from(new Set(r));for(let y of i)if(!await Oe(s,y,e))return {success:false,error:new u("IDENTIFIER_NOT_FOUND",`Identifier not found: ${y}`)};return await wr(s,e)-i.length<1?{success:false,error:new u("VALIDATION_ERROR","Cannot delete all identifiers \u2014 at least one must remain")}:(await yr(s,e,i),{success:true,identifiers:(await re(s,e)).map(y=>({id:y.id,type:y.type,value:y.value}))})}n(Ae,"bulkDeleteIdentifiers");async function kr(e,r){let t=T(r.dialect);return (await vr(t,e)).map(i=>({id:i.id,ipAddress:i.ipAddress,userAgent:i.userAgent,createdAt:i.createdAt,expiresAt:i.expiresAt}))}n(kr,"getSessions");async function Er(e,r,t){let s=T(t.dialect),i=await le(s,r);i&&i.userId===e&&await fe(s,r);}n(Er,"revokeSession");function U(e){return g(e).cookieName}n(U,"getCookieName");function ke(e){return g(e).accessCookieName}n(ke,"getAccessCookieName");function L(e,r){if(!e)return;let t=`${r}=`,s=0;for(;s<e.length;){for(;s<e.length&&e[s]===" ";)s++;let i=e.indexOf(";",s),o=i===-1?e.length:i;if(e.startsWith(t,s))return e.slice(s+t.length,o);s=o+1;}}n(L,"readCookie");function te(e,r,t){let s=t.cookie??{},i=H(g(t).refreshExpiresIn)/1e3;e.setCookie(U(t),r,{httpOnly:s.httpOnly??true,secure:s.secure??false,sameSite:s.sameSite??"strict",path:s.path??"/",maxAge:i});}n(te,"setCookieFromConfig");function Ee(e,r){let t=r.cookie??{};e.clearCookie(U(r),{path:t.path??"/"});}n(Ee,"clearCookieFromConfig");function se(e,r,t){if(!t.accessCookie)return;let s=t.accessCookie,i=H(g(t).accessExpiresIn)/1e3;e.setCookie(ke(t),r,{httpOnly:false,secure:s.secure??false,sameSite:s.sameSite??"strict",path:s.path??"/",maxAge:i});}n(se,"setAccessCookieFromConfig");function Ue(e,r){if(!r.accessCookie)return;let t=r.accessCookie;e.clearCookie(ke(r),{path:t.path??"/"});}n(Ue,"clearAccessCookieFromConfig");function Te(e,r){let t=e.headers.authorization;return t?.startsWith("Bearer ")?t.slice(7):L(e.headers.cookie,ke(r))}n(Te,"getCurrentAccessToken");function ne(e){let r=e.logger??D,t=e.loggerService??"sentri";return e.mode==="client"?zr(e.keyUri,r,t):Xr(e,r,t)}n(ne,"protect");function zr(e,r,t){return async s=>{let i=s.headers.authorization,o=i?.startsWith("Bearer ")?i.slice(7):void 0;if(!o)throw r.warn(m(t,"auth.protect.failure",{mode:"client",errorCode:"UNAUTHORIZED",requestId:s.id})),new u("UNAUTHORIZED","Missing or malformed Authorization header");try{let c=await tr(e),y=fr(o,c);s.user={id:y.id,roles:y.roles},r.info(m(t,"auth.protect.success",{mode:"client",userId:y.id,requestId:s.id}));}catch(c){let y=c instanceof u?c.code:"TOKEN_INVALID";throw r.warn(m(t,"auth.protect.failure",{mode:"client",errorCode:y,requestId:s.id})),c}}}n(zr,"protectClient");function Xr(e,r,t){return async(s,i)=>{let o=Te(s,e);if(!o)throw r.warn(m(t,"auth.protect.failure",{mode:"server",errorCode:"UNAUTHORIZED",requestId:s.id})),new u("UNAUTHORIZED","Missing or malformed Authorization header");try{let c=ce(o,e);if(e.isTokenRevoked&&await e.isTokenRevoked(c.sessionId))throw r.warn(m(t,"auth.protect.token_revoked",{mode:"server",userId:c.id,requestId:s.id})),new u("UNAUTHORIZED","Token has been revoked");s.user={id:c.id,roles:c.roles},r.info(m(t,"auth.protect.success",{mode:"server",userId:c.id,requestId:s.id}));}catch(c){if(c instanceof u&&c.code==="TOKEN_EXPIRED"){let y=L(s.headers.cookie,U(e));if(!y)throw r.warn(m(t,"auth.protect.failure",{mode:"server",errorCode:"TOKEN_EXPIRED",requestId:s.id})),new u("UNAUTHORIZED","Token expired. Please login again.");let d;try{d=await $(y,e);}catch{throw r.warn(m(t,"auth.protect.failure",{mode:"server",errorCode:"TOKEN_EXPIRED",requestId:s.id})),new u("UNAUTHORIZED","Session expired. Please login again.")}if(!d.success)throw r.warn(m(t,"auth.protect.failure",{mode:"server",errorCode:d.error.code,requestId:s.id})),new u("UNAUTHORIZED","Session expired. Please login again.");te(i,d.refreshToken,e),se(i,d.accessToken,e),i.header("X-New-Access-Token",d.accessToken),s.user=d.user,r.info(m(t,"auth.protect.auto_refresh",{mode:"server",userId:d.user.id,requestId:s.id}));}else {let y=c instanceof u?c.code:"TOKEN_INVALID";throw r.warn(m(t,"auth.protect.failure",{mode:"server",errorCode:y,requestId:s.id})),c}}}}n(Xr,"protectServer");function Tr(e,r,t){let s=`Requires one of roles: ${e.join(", ")}`;return async i=>{if(!i.user)throw r.warn(m(t,"auth.authorize.unauthenticated",{requiredRoles:e,requestId:i.id})),new u("UNAUTHORIZED","Not authenticated");let o=i.user.roles;if(!e.some(c=>o.includes(c)))throw r.warn(m(t,"auth.authorize.denied",{userId:i.user.id,userRoles:[...o],requiredRoles:e,requestId:i.id})),new u("FORBIDDEN",s);r.info(m(t,"auth.authorize.passed",{userId:i.user.id,userRoles:[...o],requiredRoles:e,requestId:i.id}));}}n(Tr,"createAuthorizeHandler");function ie(e,r){return n(function(...s){return Tr(s,e,r)},"authorize")}n(ie,"createAuthorize");function Gr(...e){return Tr(e,D,"sentri")}n(Gr,"authorize");var Wr=new u("FORBIDDEN","You do not have permission to perform this action");function xr(e,r,t){return async s=>{if(!s.user)throw r.warn(m(t,"auth.permit.unauthenticated",{requestId:s.id})),new u("UNAUTHORIZED","Not authenticated");let i=s.user.id;if(e.roles&&e.roles.length>0){let y=s.user.roles;if(e.roles.some(d=>y.includes(d))){r.info(m(t,"auth.permit.role_bypass",{userId:i,bypassedByRole:true,requestId:s.id}));return}}let o=e.check(s);if(o instanceof Promise?await o:o)r.info(m(t,"auth.permit.passed",{userId:i,requestId:s.id}));else throw r.warn(m(t,"auth.permit.denied",{userId:i,requestId:s.id})),Wr}}n(xr,"createPermitHandler");function oe(e,r){return n(function(s){return xr(typeof s=="function"?{check:s}:s,e,r)},"permit")}n(oe,"createPermit");function Zr(e){return xr(typeof e=="function"?{check:e}:e,D,"sentri")}n(Zr,"permit");var Le=class{static{n(this,"InMemoryRateLimiter");}store=new Map;constructor(){setInterval(()=>{let r=Date.now();for(let[t,s]of this.store.entries())s.expiresAt<r&&this.store.delete(t);},6e4).unref();}async consume(r,t,s){let i=Date.now(),o=this.store.get(r);if((!o||o.expiresAt<i)&&(o={count:0,expiresAt:i+s},this.store.set(r,o)),o.count>t){let c=Math.ceil((o.expiresAt-i)/1e3);throw new Error(`Rate limit exceeded. Try again in ${c} seconds.`)}o.count++;}},Fe=class{static{n(this,"RedisRateLimiter");}redis;logger;constructor(r,t){this.redis=r,this.logger=t;}async consume(r,t,s){let i=`sentri:rl:${r}`,o=Math.ceil(s/1e3);try{let c=await this.redis.multi().incr(i).expire(i,o,"NX").exec();if(!c||c.length===0)return;if(c[0][1]>t)throw new Error("Rate limit exceeded. Try again later.")}catch(c){if(c instanceof Error&&c.message.includes("Rate limit exceeded"))throw c;this.logger.error({msg:"Redis RateLimiter failed, bypassing limit",error:c});}}},V=null;async function Nr(e,r){if(V)return V;if(e)try{let{Redis:t}=await import('ioredis'),s=new t(e,{lazyConnect:!0,maxRetriesPerRequest:1});return await s.connect(),r?.info({msg:"Connected to Redis for Rate Limiting"}),V=new Fe(s,r??D),V}catch(t){r?.warn({msg:"Failed to connect to Redis for Rate Limiting, falling back to InMemoryRateLimiter",error:t});}return V=new Le,V}n(Nr,"getRateLimiter");function ae(e){return (r,t,s)=>{if(r instanceof u){s.code(r.statusCode).send({error:true,statusCode:r.statusCode,code:r.code,message:r.message,data:null});return}e?.onUnhandled?.(r),s.code(500).send({error:true,statusCode:500,code:"INTERNAL_SERVER_ERROR",message:"Internal server error",data:null});}}n(ae,"createErrorHandler");var xe=8,M=72,Ne=255,Dr=100,B=50;function _(e){return new u("VALIDATION_ERROR",e)}n(_,"badRequest");function N(e,r,t,s){e.code(r).send({error:false,statusCode:r,message:t,data:s});}n(N,"ok");function b(e,r){e.code(r.statusCode).send({error:true,statusCode:r.statusCode,code:r.code,message:r.message,data:null});}n(b,"fail");function F(e){let r=e.body;if(r==null||typeof r!="object"||Array.isArray(r))throw new u("VALIDATION_ERROR","Request body is missing or not a JSON object.");return r}n(F,"parseBody");function Jr(e,r){if(!r.apiKey)return;let t=e.headers["x-api-key"];if(typeof t!="string"||t!==r.apiKey)throw new u("UNAUTHORIZED","Invalid or missing API key")}n(Jr,"validateApiKey");function Ke(e){if(e)try{let r=e();r instanceof Promise&&r.catch(()=>{});}catch{}}n(Ke,"fireHook");function Yr(e){return e.startsWith("RS")||e.startsWith("PS")}n(Yr,"isRSA");function He(e,r){if(typeof e!="object"||e===null||Array.isArray(e))throw _(`identifiers[${r}] must be an object`);let t=e;if(typeof t.type!="string"||t.type.trim().length===0)throw _(`identifiers[${r}].type is required and must be a non-empty string`);if(t.type.length>Dr)throw _(`identifiers[${r}].type must not exceed ${Dr} characters`);if(typeof t.value!="string"||t.value.trim().length===0)throw _(`identifiers[${r}].value is required and must be a non-empty string`);if(t.value.length>Ne)throw _(`identifiers[${r}].value must not exceed ${Ne} characters`);return {type:t.type,value:t.value}}n(He,"validateIdentifierInput");function Sr(e){return async r=>{let t=e,s=g(t),i=e.logger??D,o=e.loggerService??"sentri",c=ne(e),y=ie(i,o),d=oe(i,o),l=Nr(e.redisUrl,i),v=d(a=>!!a.user);r.setErrorHandler(ae()),r.addHook("preParsing",function(a,f,p,h){if(!a.headers["content-type"]?.includes("application/json")){h(null,p);return}if(p==null){h(null,stream.Readable.from("{}"));return}let w=[];p.on("data",R=>w.push(R)),p.on("end",()=>{let R=Buffer.concat(w);h(null,R.length===0?stream.Readable.from("{}"):stream.Readable.from(R));}),p.on("error",R=>h(R));});let C=e.router?.register??(a=>me(a,t)),x=e.router?.login??(a=>pe(a,t)),O=e.router?.refresh??(a=>$(a,t)),A=e.router?.logout??(a=>a!==void 0?we(a,t):Promise.resolve()),P=e.router?.logoutAll??(a=>Ie(a,t)),j=e.router?.getUser??(a=>ye(a,t)),de=e.router?.assignRoles??((a,f)=>_e(a,f,t)),Cr=e.router?.changePassword??((a,f,p)=>Re(a,f,p,t)),Or=e.router?.bulkCreateIdentifiers??((a,f)=>ge(a,f,t)),br=e.router?.bulkUpdateIdentifiers??((a,f)=>ve(a,f,t)),Ur=e.router?.bulkDeleteIdentifiers??((a,f)=>Ae(a,f,t));Yr(s.algorithm)&&r.get("/keys",async(a,f)=>{f.header("Cache-Control","public, max-age=3600").send(er(e.secret));}),r.post("/register",async(a,f)=>{let p=Date.now();Jr(a,e);let h=F(a),{identifiers:w,password:R,roles:I}=h;if(!Array.isArray(w)||w.length===0)throw _("identifiers is required and must be a non-empty array");if(w.length>B)throw _(`identifiers must not exceed ${B} entries`);let k=w.map((G,De)=>He(G,De));if(typeof R!="string"||R.length<xe)throw _(`password is required and must be at least ${xe} characters`);if(R.length>M)throw _(`password must not exceed ${M} characters`);if(I!==void 0&&!Array.isArray(I))throw _("roles must be an array of strings when provided");if(Array.isArray(I)&&!I.every(G=>typeof G=="string"))throw _("each role must be a string");let S=Array.isArray(I)?I:void 0,E=S!==void 0?{identifiers:k,password:R,roles:S}:{identifiers:k,password:R},K=a.ip||"127.0.0.1",z=a.headers["user-agent"]||"Unknown";if(e.rateLimit!==false){let G=await l,De=typeof e.rateLimit=="object"?e.rateLimit.maxRegisterAttempts??5:5;await G.consume(`register:${K}`,De,900*1e3);}let X=await C(E,{ip:K,userAgent:z});if(!X.success){i.warn(m(o,"auth.register.failure",{errorCode:X.error.code,duration_ms:Date.now()-p,requestId:a.id})),b(f,X.error);return}i.info(m(o,"auth.register.success",{userId:X.user.id,duration_ms:Date.now()-p,requestId:a.id})),N(f,201,"User registered successfully",{user:X.user});}),r.post("/login",async(a,f)=>{let p=Date.now(),h=F(a),{identifier:w,password:R}=h;if(typeof w!="string"||w.trim().length===0)throw _("identifier is required and must be a non-empty string");if(w.length>Ne)throw _(`identifier must not exceed ${Ne} characters`);if(typeof R!="string"||R.length===0)throw _("password is required");if(R.length>M)throw _(`password must not exceed ${M} characters`);let I=w.trim(),k=a.ip||"127.0.0.1",S=a.headers["user-agent"]||"Unknown";if(e.rateLimit!==false){let K=await l,z=typeof e.rateLimit=="object"?e.rateLimit.maxLoginAttempts??5:5;await K.consume(`login:${k}`,z,900*1e3);}let E=await x({identifier:I,password:R},{ip:k,userAgent:S});if(!E.success){Ke(()=>e.hooks?.onLoginFailed?.(I,E.error,{ip:k})),i.warn(m(o,"auth.login.failure",{errorCode:E.error.code,duration_ms:Date.now()-p,requestId:a.id})),b(f,E.error);return}Ke(()=>e.hooks?.onLoginSuccess?.(E.user,{ip:k,userAgent:S})),te(f,E.refreshToken,e),se(f,E.accessToken,e),i.info(m(o,"auth.login.success",{userId:E.user.id,duration_ms:Date.now()-p,requestId:a.id})),N(f,200,"Login successful",{accessToken:E.accessToken,user:E.user});}),r.post("/refresh",async(a,f)=>{let p=Date.now(),h=L(a.headers.cookie,U(e));if(!h)throw new u("UNAUTHORIZED","Refresh token cookie is missing");let w=a.ip||"127.0.0.1",R=a.headers["user-agent"]||"Unknown",I=await O(h,{ip:w,userAgent:R});if(!I.success){Ee(f,e),i.warn(m(o,"auth.refresh.failure",{errorCode:I.error.code,duration_ms:Date.now()-p,requestId:a.id})),b(f,I.error);return}te(f,I.refreshToken,e),se(f,I.accessToken,e),i.info(m(o,"auth.refresh.success",{userId:I.user.id,duration_ms:Date.now()-p,requestId:a.id})),N(f,200,"Token refreshed",{accessToken:I.accessToken});}),r.post("/logout",async(a,f)=>{let p=Date.now(),h=L(a.headers.cookie,U(e));await A(h),Ee(f,e),Ue(f,e),i.info(m(o,"auth.logout",{duration_ms:Date.now()-p,requestId:a.id})),N(f,200,"Logged out",null);}),r.post("/logout-all",{preHandler:c},async(a,f)=>{let p=Date.now(),h=a.user.id;await P(h),Ke(()=>e.hooks?.onLogout?.(h)),Ee(f,e),Ue(f,e),i.info(m(o,"auth.logout_all",{userId:h,duration_ms:Date.now()-p,requestId:a.id})),N(f,200,"All sessions revoked",null);}),r.get("/me",{preHandler:c},async(a,f)=>{let p=Date.now(),h=await j(a.user.id);if(!h.success){i.warn(m(o,"auth.me.failure",{userId:a.user.id,errorCode:h.error.code,duration_ms:Date.now()-p,requestId:a.id})),b(f,h.error);return}i.info(m(o,"auth.me.success",{userId:h.user.id,duration_ms:Date.now()-p,requestId:a.id})),N(f,200,"OK",h.user);}),r.get("/me/identifiers",{preHandler:c},async(a,f)=>{let p=Date.now(),h=await j(a.user.id);if(!h.success){i.warn(m(o,"auth.me.identifiers.failure",{userId:a.user.id,errorCode:h.error.code,duration_ms:Date.now()-p,requestId:a.id})),b(f,h.error);return}i.info(m(o,"auth.me.identifiers.success",{userId:h.user.id,count:h.user.identifiers?.length??0,duration_ms:Date.now()-p,requestId:a.id})),N(f,200,"OK",{identifiers:h.user.identifiers??[]});}),r.post("/me/identifiers",{preHandler:[c,v]},async(a,f)=>{let p=Date.now(),h=F(a),{identifiers:w}=h;if(!Array.isArray(w)||w.length===0)throw _("identifiers is required and must be a non-empty array");if(w.length>B)throw _(`identifiers must not exceed ${B} entries`);let R=w.map((k,S)=>He(k,S)),I=await Or(a.user.id,R);if(!I.success){i.warn(m(o,"auth.identifiers.create_failure",{userId:a.user.id,errorCode:I.error.code,duration_ms:Date.now()-p,requestId:a.id})),b(f,I.error);return}i.info(m(o,"auth.identifiers.created",{userId:a.user.id,count:I.identifiers.length,duration_ms:Date.now()-p,requestId:a.id})),N(f,201,"Identifiers added successfully",{identifiers:I.identifiers});}),r.put("/me/identifiers",{preHandler:[c,v]},async(a,f)=>{let p=Date.now(),h=F(a),{identifiers:w}=h;if(!Array.isArray(w)||w.length===0)throw _("identifiers is required and must be a non-empty array");if(w.length>B)throw _(`identifiers must not exceed ${B} entries`);let R=w.map((k,S)=>{if(typeof k!="object"||k===null||Array.isArray(k))throw _(`identifiers[${S}] must be an object`);let E=k;if(typeof E.id!="string"||E.id.trim().length===0)throw _(`identifiers[${S}].id is required and must be a non-empty string`);let{type:K,value:z}=He(k,S);return {id:E.id,type:K,value:z}}),I=await br(a.user.id,R);if(!I.success){i.warn(m(o,"auth.identifiers.update_failure",{userId:a.user.id,errorCode:I.error.code,duration_ms:Date.now()-p,requestId:a.id})),b(f,I.error);return}i.info(m(o,"auth.identifiers.updated",{userId:a.user.id,count:I.identifiers.length,duration_ms:Date.now()-p,requestId:a.id})),N(f,200,"Identifiers updated successfully",{identifiers:I.identifiers});}),r.delete("/me/identifiers",{preHandler:[c,v]},async(a,f)=>{let p=Date.now(),h=F(a),{ids:w}=h;if(!Array.isArray(w)||w.length===0)throw _("ids is required and must be a non-empty array of strings");if(!w.every(I=>typeof I=="string"))throw _("each id must be a string");let R=await Ur(a.user.id,w);if(!R.success){i.warn(m(o,"auth.identifiers.delete_failure",{userId:a.user.id,errorCode:R.error.code,duration_ms:Date.now()-p,requestId:a.id})),b(f,R.error);return}i.info(m(o,"auth.identifiers.deleted",{userId:a.user.id,duration_ms:Date.now()-p,requestId:a.id})),N(f,200,"Identifiers deleted successfully",{identifiers:R.identifiers});}),r.patch("/me/password",{preHandler:[c,v]},async(a,f)=>{let p=Date.now(),h=F(a),{currentPassword:w,newPassword:R}=h;if(typeof w!="string"||w.length===0)throw _("currentPassword is required");if(typeof R!="string"||R.length<xe)throw _(`newPassword must be at least ${xe} characters`);if(R.length>M)throw _(`newPassword must not exceed ${M} characters`);if(w===R)throw _("newPassword must be different from currentPassword");let I=await Cr(a.user.id,w,R);if(!I.success){i.warn(m(o,"auth.password.change_failure",{userId:a.user.id,errorCode:I.error.code,duration_ms:Date.now()-p,requestId:a.id})),b(f,I.error);return}i.info(m(o,"auth.password.changed",{userId:a.user.id,duration_ms:Date.now()-p,requestId:a.id})),N(f,200,"Password updated successfully. All sessions have been revoked.",null);}),r.post("/users/:userId/roles",{preHandler:[c,y("admin")]},async(a,f)=>{let p=Date.now(),h=F(a),{roles:w}=h,R=a.params.userId;if(!R)throw _("userId is required");if(!Array.isArray(w)||w.length===0)throw _("roles must be a non-empty array of strings");if(!w.every(k=>typeof k=="string"))throw _("each role must be a string");let I=await de(R,w);if(!I.success){i.warn(m(o,"auth.roles.assign_failure",{targetUserId:R,errorCode:I.error.code,duration_ms:Date.now()-p,requestId:a.id})),b(f,I.error);return}i.info(m(o,"auth.roles.assigned",{targetUserId:R,roles:w,duration_ms:Date.now()-p,requestId:a.id})),N(f,200,"Roles assigned successfully",{user:I.user});}),r.get("/sessions",{preHandler:c},async(a,f)=>{let p=Date.now(),h=a.user.id,R=await(e.router?.getSessions??(I=>kr(I,t)))(h);i.info(m(o,"auth.sessions.get",{userId:h,duration_ms:Date.now()-p,requestId:a.id})),N(f,200,"OK",{sessions:R});}),r.delete("/sessions/:id",{preHandler:c},async(a,f)=>{let p=Date.now(),h=a.user.id,w=a.params.id;await(e.router?.revokeSession??((I,k)=>Er(I,k,t)))(h,w),i.info(m(o,"auth.sessions.revoke",{userId:h,sessionId:w,duration_ms:Date.now()-p,requestId:a.id})),N(f,200,"Session revoked",null);});}}n(Sr,"createAuthPlugin");function Bs(e){if(e.mode==="client"){let d={mode:"client",keyUri:e.keyUri,...e.validRoles!==void 0&&{validRoles:e.validRoles},...e.logger!==void 0&&{logger:e.logger},...e.loggerService!==void 0&&{loggerService:e.loggerService}};Xe(d);let l=d.logger??D,v=d.loggerService??"sentri",C=ie(l,v),x=oe(l,v);return {protect:n(()=>ne(d),"protect"),authorize:n((...O)=>C(...O),"authorize"),permit:n(O=>x(O),"permit"),errorHandler:n(O=>ae(O),"errorHandler")}}let{privateKey:r}=crypto.generateKeyPairSync("rsa",{modulusLength:2048,privateKeyEncoding:{type:"pkcs8",format:"pem"},publicKeyEncoding:{type:"spki",format:"pem"}}),t={mode:"server",dialect:e.dialect,secret:r,algorithm:"RS256",validRoles:e.validRoles,...e.validIdentifiers!==void 0&&{validIdentifiers:e.validIdentifiers},...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},...e.logger!==void 0&&{logger:e.logger},...e.loggerService!==void 0&&{loggerService:e.loggerService}},s=t.logger??D,i=t.loggerService??"sentri",o=g(t),c=ie(s,i),y=oe(s,i);return {protect:n(()=>ne(t),"protect"),authorize:n((...d)=>c(...d),"authorize"),permit:n(d=>y(d),"permit"),plugin:n(()=>Sr(t),"plugin"),errorHandler:n(d=>ae(d),"errorHandler"),migrate:n(()=>Ge(T(t.dialect)),"migrate"),getCurrentAccessToken:n(d=>Te(d,t),"getCurrentAccessToken"),hashPassword:n(d=>W(d,o.saltRounds),"hashPassword"),verifyPassword:n((d,l)=>Z(d,l),"verifyPassword"),signAccessToken:n(d=>Y(d,t),"signAccessToken"),signRefreshToken:n(d=>q(d,t),"signRefreshToken"),verifyAccessToken:n(d=>ce(d,t),"verifyAccessToken"),verifyRefreshToken:n(d=>Q(d,t),"verifyRefreshToken"),register:n(d=>me(d,t),"register"),login:n(d=>pe(d,t),"login"),refresh:n(d=>$(d,t),"refresh"),logout:n(d=>we(d,t),"logout"),logoutAll:n(d=>Ie(d,t),"logoutAll"),getUser:n(d=>ye(d,t),"getUser"),changePassword:n((d,l,v)=>Re(d,l,v,t),"changePassword"),assignRoles:n((d,l)=>_e(d,l,t),"assignRoles"),bulkCreateIdentifiers:n((d,l)=>ge(d,l,t),"bulkCreateIdentifiers"),bulkUpdateIdentifiers:n((d,l)=>ve(d,l,t),"bulkUpdateIdentifiers"),bulkDeleteIdentifiers:n((d,l)=>Ae(d,l,t),"bulkDeleteIdentifiers")}}n(Bs,"createAuthFastify");exports.SENTRI_ERROR_STATUS=$e;exports.SentriError=u;exports.authorize=Gr;exports.createAuthFastify=Bs;exports.createAuthPlugin=Sr;exports.createAuthorize=ie;exports.createErrorHandler=ae;exports.createPermit=oe;exports.getCurrentAccessToken=Te;exports.permit=Zr;exports.protect=ne;