sentri 4.1.0 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -259,6 +259,7 @@ interface RouterHandlers {
|
|
|
259
259
|
refresh?: (refreshToken: string) => Promise<RefreshResult>;
|
|
260
260
|
logout?: (refreshToken: string | undefined) => Promise<void>;
|
|
261
261
|
logoutAll?: (userId: string) => Promise<void>;
|
|
262
|
+
getUser?: (userId: string) => Promise<GetUserResult>;
|
|
262
263
|
assignRoles?: (userId: string, roles: string[]) => Promise<AssignRolesResult>;
|
|
263
264
|
bulkCreateIdentifiers?: (userId: string, identifiers: IdentifierInput[]) => Promise<BulkIdentifiersResult>;
|
|
264
265
|
bulkUpdateIdentifiers?: (userId: string, updates: Array<{
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import Fe from'bcrypt';import W from'jsonwebtoken';import {randomUUID,generateKeyPairSync,createPublicKey,createPrivateKey}from'crypto';import {Kysely,sql,PostgresDialect}from'kysely';import {Router}from'express';import {Redis}from'ioredis';import {Pool}from'pg';var He=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}),c=class extends Error{code;statusCode;constructor(r,t,s){super(t),this.name="SentriError",this.code=r,this.statusCode=s??He[r]??500;}};async function G(e,r=12){return Fe.hash(e,r)}async function Z(e,r){return Fe.compare(e,r)}var je=new Map,qe=new Map,Nr=3600*1e3;function Me(e){let r=je.get(e);if(!r){let t=createPrivateKey(e),s=createPublicKey(t),n=s.export({format:"jwk"}),i=Buffer.from(e).slice(0,8).toString("base64url"),u={...n,use:"sig",kid:i};r={kid:i,publicKey:s,jwk:u},je.set(e,r);}return r}function Ve(e){let{jwk:r}=Me(e);return {keys:[r]}}function Be(e){return Me(e).publicKey}async function Xe(e){let r=Date.now(),t=qe.get(e);if(t&&r-t.fetchedAt<Nr)return t.publicKey;let s=await fetch(e);if(!s.ok)throw new c("CONFIGURATION_ERROR",`Failed to fetch public key from ${e}: HTTP ${s.status}`);let n=await s.json();if(!n.keys||n.keys.length===0)throw new c("CONFIGURATION_ERROR",`No keys found in JWKS response from ${e}`);let i=n.keys[0],u=createPublicKey({key:i,format:"jwk"});return qe.set(e,{publicKey:u,fetchedAt:r}),u}var Je=new WeakMap,ze=32,Ge=10,Ze=31;function Ye(e){if(e.mode==="client"){if(!e.keyUri||e.keyUri.trim().length===0)throw new c("CONFIGURATION_ERROR","keyUri must not be empty");return}if(!e.secret||e.secret.trim().length===0)throw new c("CONFIGURATION_ERROR","secret must not be empty");if((e.algorithm??"HS256").startsWith("HS")&&e.secret.length<ze)throw new c("CONFIGURATION_ERROR",`secret must be at least ${ze} characters for HMAC algorithms`);let s=e.saltRounds??12;if(!Number.isInteger(s)||s<Ge||s>Ze)throw new c("CONFIGURATION_ERROR",`saltRounds must be an integer between ${Ge} and ${Ze}`);if(!e.validRoles||e.validRoles.length===0)throw new c("CONFIGURATION_ERROR","validRoles must contain at least one role");if(!e.dialect)throw new c("CONFIGURATION_ERROR","dialect is required in server mode")}function T(e){let r=Je.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),cookieName:e.cookie?.name??"refresh_token",accessCookieName:e.accessCookie?.name??"access_token"};return Je.set(e,t),t}var Or=/^(\d+)([smhdw])$/,Ur={s:1e3,m:6e4,h:36e5,d:864e5,w:6048e5},We=new Map;function j(e){if(typeof e=="number")return e*1e3;let r=We.get(e);if(r!==void 0)return r;let t=Or.exec(e);if(!t?.[1]||!t?.[2])throw new Error(`Invalid expiresIn: "${e}". Use e.g. "15m", "7d", "1h".`);let s=Ur[t[2]];if(s===void 0)throw new Error(`Invalid expiresIn: "${e}". Use e.g. "15m", "7d", "1h".`);let n=parseInt(t[1],10)*s;return We.set(e,n),n}var Qe=new Map,er=new Map,rr=new Map;function tr(e){return e.startsWith("RS")||e.startsWith("PS")}function sr(e){let r=Qe.get(e);return r||(r={access:`${e}:access`,refresh:`${e}:refresh`},Qe.set(e,r)),r}function Lr(e){let r=er.get(e);return r||(r=createPrivateKey(e),er.set(e,r)),r}function ir(e){let r=T(e);if(tr(r.algorithm)){let n=Lr(e.secret);return {accessKey:n,refreshKey:n}}let{access:t,refresh:s}=sr(e.secret);return {accessKey:t,refreshKey:s}}function nr(e,r){let t=T(e);if(tr(t.algorithm))return Be(e.secret);let{access:s,refresh:n}=sr(e.secret);return r==="access"?s:n}function or(e,r,t,s){let n=`${t}:${s}`,i=rr.get(n);return i||(i={expiresIn:t,algorithm:s},rr.set(n,i)),W.sign(e,r,i)}function ar(e,r,t){try{let s=W.verify(e,r,{algorithms:[t]});if(typeof s=="string"||s===null)throw new c("TOKEN_INVALID","Token payload is not an object");return s}catch(s){throw s instanceof c?s:s instanceof W.TokenExpiredError?new c("TOKEN_EXPIRED","Token has expired"):new c("TOKEN_INVALID","Token is invalid or malformed")}}function Y(e,r){let t=T(r),{accessKey:s}=ir(r);return or(e,s,t.accessExpiresIn,t.algorithm)}function Q(e,r){let t=T(r),{refreshKey:s}=ir(r);return or({sessionId:e},s,t.refreshExpiresIn,t.algorithm)}function de(e,r){let t=T(r),s=nr(r,"access");return ar(e,s,t.algorithm)}function ee(e,r){let t=T(r),s=nr(r,"refresh");return ar(e,s,t.algorithm)}function ur(e,r){try{let t=W.verify(e,r,{algorithms:["RS256","RS384","RS512","PS256","PS384","PS512"]});if(typeof t=="string"||t===null)throw new c("TOKEN_INVALID","Token payload is not an object");return t}catch(t){throw t instanceof c?t:t instanceof W.TokenExpiredError?new c("TOKEN_EXPIRED","Token has expired"):new c("TOKEN_INVALID","Token is invalid or malformed")}}function F(e){return T(e).cookieName}function q(e,r){if(!e)return;let t=`${r}=`,s=0;for(;s<e.length;){for(;s<e.length&&e[s]===" ";)s++;let n=e.indexOf(";",s),i=n===-1?e.length:n;if(e.startsWith(t,s))return e.slice(s+t.length,i);s=i+1;}}function re(e,r,t){let s=t.cookie??{},n=T(t),i=j(n.refreshExpiresIn);e.cookie(F(t),r,{httpOnly:s.httpOnly??true,secure:s.secure??false,sameSite:s.sameSite??"strict",path:s.path??"/",maxAge:i});}function ce(e,r){let t=r.cookie??{};e.clearCookie(F(r),{path:t.path??"/"});}function ke(e){return T(e).accessCookieName}function te(e,r,t){if(!t.accessCookie)return;let s=t.accessCookie,n=T(t),i=j(n.accessExpiresIn);e.cookie(ke(t),r,{httpOnly:false,secure:s.secure??false,sameSite:s.sameSite??"strict",path:s.path??"/",maxAge:i});}function Se(e,r){if(!r.accessCookie)return;let t=r.accessCookie;e.clearCookie(ke(r),{path:t.path??"/"});}function se(e,r){let t=e.headers.authorization;return t?.startsWith("Bearer ")?t.slice(7):q(e.headers.cookie,ke(r))}var dr=new Map;function k(e){let r=dr.get(e);return r||(r=new Kysely({dialect:e}),dr.set(e,r)),r}function Ee(e){try{return JSON.parse(e)}catch{return []}}function Fr(e){return JSON.stringify(e)}function lr(e){return {id:e.id,userId:e.user_id,type:e.type,value:e.value,isPrimary:e.is_primary===1,createdAt:new Date(e.created_at)}}async function ie(e,r){let t=await e.selectFrom("sentri_identifiers as i_login").innerJoin("sentri_users as u","u.id","i_login.user_id").innerJoin("sentri_identifiers as i_primary",s=>s.onRef("i_primary.user_id","=","u.id").on("i_primary.is_primary","=",1)).select(["u.id","u.password_hash","u.roles","i_primary.value as primary_value","i_primary.type as primary_type"]).where("i_login.value","=",r).executeTakeFirst();return t?{id:t.id,identifier:t.primary_value,identifierType:t.primary_type,passwordHash:t.password_hash,roles:Ee(t.roles)}:null}async function le(e,r){let t=await e.selectFrom("sentri_users as u").innerJoin("sentri_identifiers as i",s=>s.onRef("i.user_id","=","u.id").on("i.is_primary","=",1)).select(["u.id","u.password_hash","u.roles","i.value as primary_value","i.type as primary_type"]).where("u.id","=",r).executeTakeFirst();return t?{id:t.id,identifier:t.primary_value,identifierType:t.primary_type,passwordHash:t.password_hash,roles:Ee(t.roles)}:null}async function fr(e,r,t){let s=t.map(n=>({id:randomUUID(),user_id:r,type:n.type,value:n.value,is_primary:n.isPrimary?1:0}));return await e.insertInto("sentri_identifiers").values(s).execute(),s.map(n=>({id:n.id,userId:n.user_id,type:n.type,value:n.value,isPrimary:n.is_primary===1,createdAt:new Date}))}async function $(e,r){return (await e.selectFrom("sentri_identifiers").selectAll().where("user_id","=",r).orderBy("created_at","asc").execute()).map(lr)}async function fe(e,r,t){let s=await e.selectFrom("sentri_identifiers").selectAll().where("id","=",r).where("user_id","=",t).executeTakeFirst();return s?lr(s):null}async function pr(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)}async function gr(e,r,t,s){await e.updateTable("sentri_identifiers").set({type:s.type,value:s.value}).where("id","=",r).where("user_id","=",t).execute();}async function mr(e,r,t){await e.deleteFrom("sentri_identifiers").where("user_id","=",r).where("id","in",t).execute();}async function hr(e,r,t){await e.transaction().execute(async s=>{await s.updateTable("sentri_identifiers").set({is_primary:0}).where("user_id","=",r).execute(),await s.updateTable("sentri_identifiers").set({is_primary:1}).where("id","=",t).where("user_id","=",r).execute();});}async function yr(e,r,t){await e.updateTable("sentri_users").set({password_hash:t}).where("id","=",r).execute();}async function Rr(e,r,t){await e.updateTable("sentri_users").set({roles:Fr(t)}).where("id","=",r).execute();}async function be(e,r){let t=randomUUID();return await e.insertInto("sentri_sessions").values({id:t,user_id:r.userId,expires_at:r.expiresAt}).execute(),{id:t}}async function wr(e,r){let t=await e.selectFrom("sentri_sessions as s").innerJoin("sentri_users as u","u.id","s.user_id").innerJoin("sentri_identifiers as i",s=>s.onRef("i.user_id","=","u.id").on("i.is_primary","=",1)).select(["s.id as session_id","s.user_id","s.expires_at","s.created_at as session_created_at","u.id as user_id_col","u.password_hash","u.roles","i.value as primary_identifier","i.type as primary_identifier_type"]).where("s.id","=",r).executeTakeFirst();return t?{id:t.session_id,userId:t.user_id,expiresAt:new Date(t.expires_at),createdAt:new Date(t.session_created_at),user:{id:t.user_id_col,identifier:t.primary_identifier,identifierType:t.primary_identifier_type,passwordHash:t.password_hash,roles:Ee(t.roles)}}:null}async function pe(e,r){await e.deleteFrom("sentri_sessions").where("id","=",r).execute();}async function xe(e,r){await e.deleteFrom("sentri_sessions").where("user_id","=",r).execute();}async function ne(e,r){let t=T(r),s=k(r.dialect),n=e.roles??[],i=n.filter(v=>!t.validRolesSet.has(v));if(i.length>0)return {success:false,error:new c("INVALID_ROLE",`Invalid roles: ${i.join(", ")}`)};if(!e.identifiers||e.identifiers.length===0)return {success:false,error:new c("VALIDATION_ERROR","At least one identifier is required")};let u=e.identifiers.map(v=>({type:v.type.trim(),value:v.value.trim()}));if(new Set(u.map(v=>v.value)).size!==u.length)return {success:false,error:new c("VALIDATION_ERROR","Duplicate identifier values in request")};for(let v of u)if(await ie(s,v.value))return {success:false,error:new c("IDENTIFIER_ALREADY_EXISTS",`Identifier already taken: ${v.value}`)};let d=await G(e.password,t.saltRounds),{userId:g,identifierRows:h}=await s.transaction().execute(async v=>{let P=randomUUID();await v.insertInto("sentri_users").values({id:P,password_hash:d,roles:JSON.stringify(n)}).execute();let K=u.map((L,Ce)=>({id:randomUUID(),user_id:P,type:L.type,value:L.value,is_primary:Ce===0?1:0}));return await v.insertInto("sentri_identifiers").values(K).execute(),{userId:P,identifierRows:K}}),x=h.map(v=>({id:v.id,type:v.type,value:v.value,isPrimary:v.is_primary===1})),S=x[0];return {success:true,user:{id:g,identifier:S.value,identifierType:S.type,roles:n,identifiers:x}}}async function ge(e,r){let t=T(r),s=k(r.dialect),n=await ie(s,e.identifier.trim());if(!n)return {success:false,error:new c("INVALID_CREDENTIALS","Invalid credentials")};if(!await Z(e.password,n.passwordHash))return {success:false,error:new c("INVALID_CREDENTIALS","Invalid credentials")};let u=new Date(Date.now()+j(t.refreshExpiresIn)),o=await be(s,{userId:n.id,expiresAt:u}),d={id:n.id,identifier:n.identifier,identifierType:n.identifierType,roles:n.roles},g=Y({id:n.id,identifier:n.identifier,identifierType:n.identifierType,roles:n.roles,sessionId:o.id},r),h=Q(o.id,r);return {success:true,accessToken:g,refreshToken:h,user:d}}async function M(e,r){let t=T(r),s=k(r.dialect),n;try{({sessionId:n}=ee(e,r));}catch(x){return x instanceof c?{success:false,error:x}:{success:false,error:new c("TOKEN_INVALID","Invalid refresh token")}}let i=await wr(s,n);if(!i)return {success:false,error:new c("UNAUTHORIZED","Session not found or revoked")};if(i.expiresAt.getTime()<Date.now())return await pe(s,n),{success:false,error:new c("TOKEN_EXPIRED","Session has expired")};await pe(s,n);let u=new Date(Date.now()+j(t.refreshExpiresIn)),o=await be(s,{userId:i.userId,expiresAt:u}),d={id:i.user.id,identifier:i.user.identifier,identifierType:i.user.identifierType,roles:i.user.roles},g=Y({...d,sessionId:o.id},r),h=Q(o.id,r);return {success:true,accessToken:g,refreshToken:h,user:d}}async function me(e,r){let t=k(r.dialect),s;try{({sessionId:s}=ee(e,r));}catch{return}await pe(t,s);}async function he(e,r){let t=k(r.dialect);await xe(t,e);}async function Ar(e,r){let t=k(r.dialect),s=await le(t,e);if(!s)return {success:false,error:new c("USER_NOT_FOUND","User not found")};let n=await $(t,e);return {success:true,user:{id:s.id,identifier:s.identifier,identifierType:s.identifierType,roles:s.roles,identifiers:n.map(i=>({id:i.id,type:i.type,value:i.value,isPrimary:i.isPrimary}))}}}async function ye(e,r,t,s){let n=T(s),i=k(s.dialect),u=await le(i,e);if(!u)return {success:false,error:new c("USER_NOT_FOUND","User not found")};if(!await Z(r,u.passwordHash))return {success:false,error:new c("INVALID_CREDENTIALS","Invalid credentials")};let d=await G(t,n.saltRounds);return await yr(i,e,d),await xe(i,e),{success:true}}async function Re(e,r,t){let s=T(t),n=k(t.dialect),i=r.filter(g=>!s.validRolesSet.has(g));if(i.length>0)return {success:false,error:new c("INVALID_ROLE",`Invalid roles: ${i.join(", ")}`)};let u=await le(n,e);if(!u)return {success:false,error:new c("USER_NOT_FOUND","User not found")};let o=new Set(u.roles);for(let g of r)o.add(g);let d=Array.from(o);return await Rr(n,e,d),{success:true,user:{id:u.id,identifier:u.identifier,identifierType:u.identifierType,roles:d}}}async function we(e,r,t){let s=k(t.dialect);if(r.length===0)return {success:false,error:new c("VALIDATION_ERROR","At least one identifier is required")};let n=r.map(d=>({type:d.type.trim(),value:d.value.trim()}));if(new Set(n.map(d=>d.value)).size!==n.length)return {success:false,error:new c("VALIDATION_ERROR","Duplicate identifier values in request")};for(let d of n)if(await ie(s,d.value))return {success:false,error:new c("IDENTIFIER_ALREADY_EXISTS",`Identifier already taken: ${d.value}`)};await fr(s,e,n.map(d=>({...d,isPrimary:false})));return {success:true,identifiers:(await $(s,e)).map(d=>({id:d.id,type:d.type,value:d.value,isPrimary:d.isPrimary}))}}async function Ie(e,r,t){let s=k(t.dialect);if(r.length===0)return {success:false,error:new c("VALIDATION_ERROR","At least one update is required")};let n=r.map(o=>({id:o.id,type:o.type.trim(),value:o.value.trim()}));if(new Set(n.map(o=>o.value)).size!==n.length)return {success:false,error:new c("VALIDATION_ERROR","Duplicate identifier values in request")};for(let o of n){let d=await fe(s,o.id,e);if(!d)return {success:false,error:new c("IDENTIFIER_NOT_FOUND",`Identifier not found: ${o.id}`)};if(d.value!==o.value&&await ie(s,o.value))return {success:false,error:new c("IDENTIFIER_ALREADY_EXISTS",`Identifier already taken: ${o.value}`)}}for(let o of n)await gr(s,o.id,e,{type:o.type,value:o.value});return {success:true,identifiers:(await $(s,e)).map(o=>({id:o.id,type:o.type,value:o.value,isPrimary:o.isPrimary}))}}async function Ae(e,r,t){let s=k(t.dialect);if(r.length===0)return {success:false,error:new c("VALIDATION_ERROR","At least one ID is required")};for(let u of r)if(!await fe(s,u,e))return {success:false,error:new c("IDENTIFIER_NOT_FOUND",`Identifier not found: ${u}`)};return await pr(s,e)-r.length<1?{success:false,error:new c("VALIDATION_ERROR","Cannot delete all identifiers \u2014 at least one must remain")}:(await mr(s,e,r),{success:true,identifiers:(await $(s,e)).map(u=>({id:u.id,type:u.type,value:u.value,isPrimary:u.isPrimary}))})}async function ve(e,r,t){let s=k(t.dialect);return await fe(s,r,e)?(await hr(s,e,r),{success:true,identifiers:(await $(s,e)).map(u=>({id:u.id,type:u.type,value:u.value,isPrimary:u.isPrimary}))}):{success:false,error:new c("IDENTIFIER_NOT_FOUND",`Identifier not found: ${r}`)}}var O={info:()=>{},warn:()=>{},error:()=>{}};function m(e,r,t){return {service:e,event:r,...t}}function E(e){let r=e.logger??O,t=e.loggerService??"sentri";return e.mode==="client"?jr(e.keyUri,r,t):qr(e,r,t)}function jr(e,r,t){return async(s,n,i)=>{let u=s.headers.authorization,o=u?.startsWith("Bearer ")?u.slice(7):void 0,d=s.requestId;if(!o)return r.warn(m(t,"auth.protect.failure",{mode:"client",errorCode:"UNAUTHORIZED",...d!==void 0&&{requestId:d}})),i(new c("UNAUTHORIZED","Missing or malformed Authorization header"));try{let g=await Xe(e),h=ur(o,g);s.user={id:h.id,identifier:h.identifier,identifierType:h.identifierType,roles:h.roles},r.info(m(t,"auth.protect.success",{mode:"client",userId:h.id,...d!==void 0&&{requestId:d}})),i();}catch(g){let h=g instanceof c?g.code:"TOKEN_INVALID";r.warn(m(t,"auth.protect.failure",{mode:"client",errorCode:h,...d!==void 0&&{requestId:d}})),i(g);}}}function qr(e,r,t){return async(s,n,i)=>{let u=se(s,e),o=s.requestId;if(!u)return r.warn(m(t,"auth.protect.failure",{mode:"server",errorCode:"UNAUTHORIZED",...o!==void 0&&{requestId:o}})),i(new c("UNAUTHORIZED","Missing or malformed Authorization header"));try{let d=de(u,e);if(e.isTokenRevoked&&await e.isTokenRevoked(d.sessionId))return r.warn(m(t,"auth.protect.token_revoked",{mode:"server",userId:d.id,...o!==void 0&&{requestId:o}})),i(new c("UNAUTHORIZED","Token has been revoked"));s.user={id:d.id,identifier:d.identifier,identifierType:d.identifierType,roles:d.roles},r.info(m(t,"auth.protect.success",{mode:"server",userId:d.id,...o!==void 0&&{requestId:o}})),i();}catch(d){if(d instanceof c&&d.code==="TOKEN_EXPIRED"){let g=q(s.headers.cookie,F(e));if(!g)return r.warn(m(t,"auth.protect.failure",{mode:"server",errorCode:"TOKEN_EXPIRED",...o!==void 0&&{requestId:o}})),i(new c("UNAUTHORIZED","Token expired. Please login again."));try{let h=await M(g,e);if(!h.success)return r.warn(m(t,"auth.protect.failure",{mode:"server",errorCode:h.error.code,...o!==void 0&&{requestId:o}})),i(new c("UNAUTHORIZED","Session expired. Please login again."));re(n,h.refreshToken,e),te(n,h.accessToken,e),n.setHeader("X-New-Access-Token",h.accessToken),s.user=h.user,r.info(m(t,"auth.protect.auto_refresh",{mode:"server",userId:h.user.id,...o!==void 0&&{requestId:o}})),i();}catch{r.warn(m(t,"auth.protect.failure",{mode:"server",errorCode:"TOKEN_EXPIRED",...o!==void 0&&{requestId:o}})),i(new c("UNAUTHORIZED","Session expired. Please login again."));}}else {let g=d instanceof c?d.code:"TOKEN_INVALID";r.warn(m(t,"auth.protect.failure",{mode:"server",errorCode:g,...o!==void 0&&{requestId:o}})),i(d);}}}}function vr(e,r,t){let s=`Requires one of roles: ${e.join(", ")}`;return (n,i,u)=>{let o=n.requestId;if(!n.user)return r.warn(m(t,"auth.authorize.unauthenticated",{requiredRoles:e,...o!==void 0&&{requestId:o}})),u(new c("UNAUTHORIZED","Not authenticated"));let d=n.user.roles;if(!e.some(g=>d.includes(g)))return r.warn(m(t,"auth.authorize.denied",{userId:n.user.id,userRoles:[...d],requiredRoles:e,...o!==void 0&&{requestId:o}})),u(new c("FORBIDDEN",s));r.info(m(t,"auth.authorize.passed",{userId:n.user.id,userRoles:[...d],requiredRoles:e,...o!==void 0&&{requestId:o}})),u();}}function oe(e,r){return function(...s){return vr(s,e,r)}}function De(...e){return vr(e,O,"sentri")}var $r=new c("FORBIDDEN","You do not have permission to perform this action");function _r(e,r,t){return async(s,n,i)=>{let u=s.requestId;if(!s.user)return r.warn(m(t,"auth.permit.unauthenticated",{...u!==void 0&&{requestId:u}})),i(new c("UNAUTHORIZED","Not authenticated"));let o=s.user.id;if(e.roles&&e.roles.length>0){let d=s.user.roles;if(e.roles.some(g=>d.includes(g)))return r.info(m(t,"auth.permit.role_bypass",{userId:o,bypassedByRole:true,...u!==void 0&&{requestId:u}})),i()}try{let d=e.check(s);(d instanceof Promise?await d:d)?(r.info(m(t,"auth.permit.passed",{userId:o,...u!==void 0&&{requestId:u}})),i()):(r.warn(m(t,"auth.permit.denied",{userId:o,...u!==void 0&&{requestId:u}})),i($r));}catch(d){i(d);}}}function ae(e,r){return function(s){return _r(typeof s=="function"?{check:s}:s,e,r)}}function Pe(e){return _r(typeof e=="function"?{check:e}:e,O,"sentri")}function V(e){return (r,t,s,n)=>{if(r instanceof c){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});}}var _e=8,B=72,Te=255,Tr=100,X=50;function w(e){return new c("VALIDATION_ERROR",e)}function b(e,r,t,s){e.status(r).json({error:false,statusCode:r,message:t,data:s});}function N(e,r){e.status(r.statusCode).json({error:true,statusCode:r.statusCode,code:r.code,message:r.message,data:null});}function U(e){if(e==null||typeof e!="object"||Array.isArray(e))throw new c("VALIDATION_ERROR","Request body is missing or not a JSON object. Did you apply express.json()?");return e}function Vr(e,r){if(!r.apiKey)return;let t=e.headers["x-api-key"];if(typeof t!="string"||t!==r.apiKey)throw new c("UNAUTHORIZED","Invalid or missing API key")}function Ne(e){if(e)try{let r=e();r instanceof Promise&&r.catch(()=>{});}catch{}}function Br(e){return e.startsWith("RS")||e.startsWith("PS")}function Oe(e,r){if(typeof e!="object"||e===null||Array.isArray(e))throw w(`identifiers[${r}] must be an object`);let t=e;if(typeof t.type!="string"||t.type.trim().length===0)throw w(`identifiers[${r}].type is required and must be a non-empty string`);if(t.type.length>Tr)throw w(`identifiers[${r}].type must not exceed ${Tr} characters`);if(typeof t.value!="string"||t.value.trim().length===0)throw w(`identifiers[${r}].value is required and must be a non-empty string`);if(t.value.length>Te)throw w(`identifiers[${r}].value must not exceed ${Te} characters`);return {type:t.type,value:t.value}}function C(e){return e.requestId!==void 0?{requestId:e.requestId}:{}}function Cr(e){let r=Router(),t=e,s=T(t),n=e.logger??O,i=e.loggerService??"sentri",u=oe(n,i),o=ae(n,i),d=e.router?.register??(a=>ne(a,t)),g=e.router?.login??(a=>ge(a,t)),h=e.router?.refresh??(a=>M(a,t)),x=e.router?.logout??(a=>a!==void 0?me(a,t):Promise.resolve()),S=e.router?.logoutAll??(a=>he(a,t)),v=e.router?.assignRoles??((a,l)=>Re(a,l,t)),P=e.router?.changePassword??((a,l,_)=>ye(a,l,_,t)),K=e.router?.bulkCreateIdentifiers??((a,l)=>we(a,l,t)),L=e.router?.bulkUpdateIdentifiers??((a,l)=>Ie(a,l,t)),Ce=e.router?.bulkDeleteIdentifiers??((a,l)=>Ae(a,l,t)),xr=e.router?.changePrimaryIdentifier??((a,l)=>ve(a,l,t));Br(s.algorithm)&&r.get("/keys",(a,l)=>{l.setHeader("Cache-Control","public, max-age=3600"),l.json(Ve(e.secret));}),r.post("/register",async(a,l,_)=>{let I=Date.now();try{Vr(a,e);let p=U(a.body),{identifiers:f,password:y,roles:R}=p;if(!Array.isArray(f)||f.length===0)throw w("identifiers is required and must be a non-empty array");if(f.length>X)throw w(`identifiers must not exceed ${X} entries`);let A=f.map((z,Dr)=>Oe(z,Dr));if(typeof y!="string"||y.length<_e)throw w(`password is required and must be at least ${_e} characters`);if(y.length>B)throw w(`password must not exceed ${B} characters`);if(R!==void 0&&!Array.isArray(R))throw w("roles must be an array of strings when provided");if(Array.isArray(R)&&!R.every(z=>typeof z=="string"))throw w("each role must be a string");let D=Array.isArray(R)?R:void 0,H=await d(D!==void 0?{identifiers:A,password:y,roles:D}:{identifiers:A,password:y});if(!H.success){n.warn(m(i,"auth.register.failure",{errorCode:H.error.code,duration_ms:Date.now()-I,...C(a)})),N(l,H.error);return}n.info(m(i,"auth.register.success",{userId:H.user.id,duration_ms:Date.now()-I,...C(a)})),b(l,201,"User registered successfully",{user:H.user});}catch(p){_(p);}}),r.post("/login",async(a,l,_)=>{let I=Date.now();try{let p=U(a.body),{identifier:f,password:y}=p;if(typeof f!="string"||f.trim().length===0)throw w("identifier is required and must be a non-empty string");if(f.length>Te)throw w(`identifier must not exceed ${Te} characters`);if(typeof y!="string"||y.length===0)throw w("password is required");if(y.length>B)throw w(`password must not exceed ${B} characters`);let R=f.trim(),A=await g({identifier:R,password:y});if(!A.success){Ne(()=>e.hooks?.onFailedLogin?.(R,A.error)),n.warn(m(i,"auth.login.failure",{errorCode:A.error.code,duration_ms:Date.now()-I,...C(a)})),N(l,A.error);return}Ne(()=>e.hooks?.onLogin?.(A.user)),re(l,A.refreshToken,e),te(l,A.accessToken,e),n.info(m(i,"auth.login.success",{userId:A.user.id,duration_ms:Date.now()-I,...C(a)})),b(l,200,"Login successful",{accessToken:A.accessToken,user:A.user});}catch(p){_(p);}}),r.post("/refresh",async(a,l,_)=>{let I=Date.now();try{let p=q(a.headers.cookie,F(e));if(!p)throw new c("UNAUTHORIZED","Refresh token cookie is missing");let f=await h(p);if(!f.success){ce(l,e),n.warn(m(i,"auth.refresh.failure",{errorCode:f.error.code,duration_ms:Date.now()-I,...C(a)})),N(l,f.error);return}re(l,f.refreshToken,e),te(l,f.accessToken,e),n.info(m(i,"auth.refresh.success",{userId:f.user.id,duration_ms:Date.now()-I,...C(a)})),b(l,200,"Token refreshed",{accessToken:f.accessToken});}catch(p){_(p);}}),r.post("/logout",async(a,l,_)=>{let I=Date.now();try{let p=q(a.headers.cookie,F(e));await x(p),ce(l,e),Se(l,e),n.info(m(i,"auth.logout",{duration_ms:Date.now()-I,...C(a)})),b(l,200,"Logged out",null);}catch(p){_(p);}}),r.post("/logout-all",E(e),async(a,l,_)=>{let I=Date.now();try{let p=a.user.id;await S(p),Ne(()=>e.hooks?.onLogout?.(p)),ce(l,e),Se(l,e),n.info(m(i,"auth.logout_all",{userId:p,duration_ms:Date.now()-I,...C(a)})),b(l,200,"All sessions revoked",null);}catch(p){_(p);}}),r.get("/me",E(e),(a,l)=>{b(l,200,"OK",a.user);});let J=o(a=>!!a.user);return r.post("/me/identifiers",E(e),J,async(a,l,_)=>{let I=Date.now();try{let p=U(a.body),{identifiers:f}=p;if(!Array.isArray(f)||f.length===0)throw w("identifiers is required and must be a non-empty array");if(f.length>X)throw w(`identifiers must not exceed ${X} entries`);let y=f.map((A,D)=>Oe(A,D)),R=await K(a.user.id,y);if(!R.success){n.warn(m(i,"auth.identifiers.create_failure",{userId:a.user.id,errorCode:R.error.code,duration_ms:Date.now()-I,...C(a)})),N(l,R.error);return}n.info(m(i,"auth.identifiers.created",{userId:a.user.id,count:R.identifiers.length,duration_ms:Date.now()-I,...C(a)})),b(l,201,"Identifiers added successfully",{identifiers:R.identifiers});}catch(p){_(p);}}),r.put("/me/identifiers",E(e),J,async(a,l,_)=>{let I=Date.now();try{let p=U(a.body),{identifiers:f}=p;if(!Array.isArray(f)||f.length===0)throw w("identifiers is required and must be a non-empty array");if(f.length>X)throw w(`identifiers must not exceed ${X} entries`);let y=f.map((A,D)=>{if(typeof A!="object"||A===null||Array.isArray(A))throw w(`identifiers[${D}] must be an object`);let ue=A;if(typeof ue.id!="string"||ue.id.trim().length===0)throw w(`identifiers[${D}].id is required and must be a non-empty string`);let{type:H,value:z}=Oe(A,D);return {id:ue.id,type:H,value:z}}),R=await L(a.user.id,y);if(!R.success){n.warn(m(i,"auth.identifiers.update_failure",{userId:a.user.id,errorCode:R.error.code,duration_ms:Date.now()-I,...C(a)})),N(l,R.error);return}n.info(m(i,"auth.identifiers.updated",{userId:a.user.id,count:R.identifiers.length,duration_ms:Date.now()-I,...C(a)})),b(l,200,"Identifiers updated successfully",{identifiers:R.identifiers});}catch(p){_(p);}}),r.delete("/me/identifiers",E(e),J,async(a,l,_)=>{let I=Date.now();try{let p=U(a.body),{ids:f}=p;if(!Array.isArray(f)||f.length===0)throw w("ids is required and must be a non-empty array of strings");if(!f.every(R=>typeof R=="string"))throw w("each id must be a string");let y=await Ce(a.user.id,f);if(!y.success){n.warn(m(i,"auth.identifiers.delete_failure",{userId:a.user.id,errorCode:y.error.code,duration_ms:Date.now()-I,...C(a)})),N(l,y.error);return}n.info(m(i,"auth.identifiers.deleted",{userId:a.user.id,duration_ms:Date.now()-I,...C(a)})),b(l,200,"Identifiers deleted successfully",{identifiers:y.identifiers});}catch(p){_(p);}}),r.patch("/me/identifiers/primary",E(e),J,async(a,l,_)=>{let I=Date.now();try{let p=U(a.body),{id:f}=p;if(typeof f!="string"||f.trim().length===0)throw w("id is required and must be a non-empty string");let y=await xr(a.user.id,f.trim());if(!y.success){n.warn(m(i,"auth.identifiers.primary_change_failure",{userId:a.user.id,errorCode:y.error.code,duration_ms:Date.now()-I,...C(a)})),N(l,y.error);return}n.info(m(i,"auth.identifiers.primary_changed",{userId:a.user.id,duration_ms:Date.now()-I,...C(a)})),b(l,200,"Primary identifier updated successfully",{identifiers:y.identifiers});}catch(p){_(p);}}),r.patch("/me/password",E(e),J,async(a,l,_)=>{let I=Date.now();try{let p=U(a.body),{currentPassword:f,newPassword:y}=p;if(typeof f!="string"||f.length===0)throw w("currentPassword is required");if(typeof y!="string"||y.length<_e)throw w(`newPassword must be at least ${_e} characters`);if(y.length>B)throw w(`newPassword must not exceed ${B} characters`);if(f===y)throw w("newPassword must be different from currentPassword");let R=await P(a.user.id,f,y);if(!R.success){n.warn(m(i,"auth.password.change_failure",{userId:a.user.id,errorCode:R.error.code,duration_ms:Date.now()-I,...C(a)})),N(l,R.error);return}n.info(m(i,"auth.password.changed",{userId:a.user.id,duration_ms:Date.now()-I,...C(a)})),b(l,200,"Password updated successfully. All sessions have been revoked.",null);}catch(p){_(p);}}),r.post("/users/:userId/roles",E(e),u("admin"),async(a,l,_)=>{let I=Date.now();try{let p=U(a.body),{roles:f}=p,y=a.params.userId,R=typeof y=="string"?y:void 0;if(!R)throw w("userId is required");if(!Array.isArray(f)||f.length===0)throw w("roles must be a non-empty array of strings");if(!f.every(D=>typeof D=="string"))throw w("each role must be a string");let A=await v(R,f);if(!A.success){n.warn(m(i,"auth.roles.assign_failure",{targetUserId:R,errorCode:A.error.code,duration_ms:Date.now()-I,...C(a)})),N(l,A.error);return}n.info(m(i,"auth.roles.assigned",{targetUserId:R,roles:f,duration_ms:Date.now()-I,...C(a)})),b(l,200,"Roles assigned successfully",{user:A.user});}catch(p){_(p);}}),r.use(V()),r}var kr=new Map;function Sr(e){let r=kr.get(e);return r||(r=new Redis(e,{lazyConnect:false,enableOfflineQueue:false}),kr.set(e,r)),r}function Ue(e){let r=e?.ttl??3e5,t=(e?.header??"X-Idempotency-Key").toLowerCase(),s=new Set((e?.methods??["POST","PUT","PATCH"]).map(i=>i.toUpperCase())),n=e?.redisUrl;return n?Jr(n,r,t,s):zr(r,t,s,e?.maxSize??1e4)}function Jr(e,r,t,s){let n=Sr(e),i="sentri:idempotency:";return async(u,o,d)=>{let g=u.headers[t];if(!g||typeof g!="string"||!s.has(u.method))return d();u.requestId=g,o.setHeader("X-Request-Id",g);let h=await n.get(`${i}${g}`);if(h){let S=JSON.parse(h);return o.setHeader("X-Idempotent-Replayed","true"),o.status(S.statusCode).json(S.body)}let x=o.json.bind(o);o.json=function(v){if(o.statusCode>=200&&o.statusCode<300){let P={statusCode:o.statusCode,body:v,expiresAt:Date.now()+r};n.set(`${i}${g}`,JSON.stringify(P),"PX",r).catch(()=>{});}return x(v)},d();}}function zr(e,r,t,s){let n=Math.max(e,5e3),i=new Map,u=setInterval(()=>{let o=Date.now();for(let[d,g]of i)g.expiresAt<=o&&i.delete(d);},n);return typeof u=="object"&&u!==null&&"unref"in u&&u.unref(),(o,d,g)=>{let h=o.headers[r];if(!h||typeof h!="string"||!t.has(o.method))return g();o.requestId=h,d.setHeader("X-Request-Id",h);let x=Date.now(),S=i.get(h);if(S&&S.expiresAt>x)return d.setHeader("X-Idempotent-Replayed","true"),d.status(S.statusCode).json(S.body);let v=d.json.bind(d);d.json=function(K){if(d.statusCode>=200&&d.statusCode<300){if(i.size>=s){let L=i.keys().next().value;L!==void 0&&i.delete(L);}i.set(h,{statusCode:d.statusCode,body:K,expiresAt:Date.now()+e});}return v(K)},g();}}async function Er(e){await e.schema.createTable("sentri_users").ifNotExists().addColumn("id","varchar(36)",r=>r.primaryKey().notNull()).addColumn("password_hash","varchar(255)",r=>r.notNull()).addColumn("roles","text",r=>r.notNull().defaultTo("[]")).addColumn("created_at","timestamp",r=>r.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)).execute(),await e.schema.createTable("sentri_sessions").ifNotExists().addColumn("id","varchar(36)",r=>r.primaryKey().notNull()).addColumn("user_id","varchar(36)",r=>r.notNull().references("sentri_users.id").onDelete("cascade")).addColumn("expires_at","timestamp",r=>r.notNull()).addColumn("created_at","timestamp",r=>r.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)).execute(),await e.schema.createTable("sentri_identifiers").ifNotExists().addColumn("id","varchar(36)",r=>r.primaryKey().notNull()).addColumn("user_id","varchar(36)",r=>r.notNull().references("sentri_users.id").onDelete("cascade")).addColumn("type","varchar(100)",r=>r.notNull()).addColumn("value","varchar(255)",r=>r.notNull().unique()).addColumn("is_primary","integer",r=>r.notNull().defaultTo(0)).addColumn("created_at","timestamp",r=>r.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)).execute();}function Le(e){if(Ye(e),e.mode==="client"){let i=e.logger?oe(e.logger,e.loggerService??"sentri"):De,u=e.logger?ae(e.logger,e.loggerService??"sentri"):Pe;return {protect:()=>E(e),authorize:(...o)=>i(...o),permit:o=>u(o),errorHandler:o=>V(o)}}let r=e,t=T(r),s=r.logger?oe(r.logger,r.loggerService??"sentri"):De,n=r.logger?ae(r.logger,r.loggerService??"sentri"):Pe;return {protect:()=>E(r),authorize:(...i)=>s(...i),permit:i=>n(i),hashPassword:i=>G(i,t.saltRounds),verifyPassword:(i,u)=>Z(i,u),signAccessToken:i=>Y(i,r),signRefreshToken:i=>Q(i,r),verifyAccessToken:i=>de(i,r),verifyRefreshToken:i=>ee(i,r),getCurrentAccessToken:i=>se(i,r),router:()=>Cr(r),migrate:()=>Er(k(r.dialect)),errorHandler:i=>V(i),idempotencyMiddleware:i=>Ue({...i,...r.redisUrl!==void 0&&{redisUrl:r.redisUrl}}),register:i=>ne(i,r),login:i=>ge(i,r),refresh:i=>M(i,r),logout:i=>me(i,r),logoutAll:i=>he(i,r),getUser:i=>Ar(i,r),changePassword:(i,u,o)=>ye(i,u,o,r),assignRoles:(i,u)=>Re(i,u,r),bulkCreateIdentifiers:(i,u)=>we(i,u,r),bulkUpdateIdentifiers:(i,u)=>Ie(i,u,r),bulkDeleteIdentifiers:(i,u)=>Ae(i,u,r),changePrimaryIdentifier:(i,u)=>ve(i,u,r)}}function br(e){return new PostgresDialect({pool:new Pool(e)})}function Yr(e){let{privateKey:r}=generateKeyPairSync("rsa",{modulusLength:2048,privateKeyEncoding:{type:"pkcs8",format:"pem"},publicKeyEncoding:{type:"spki",format:"pem"}}),t={mode:"server",dialect:br(e.db),secret:r,algorithm:"RS256",validRoles:e.validRoles,...e.accessExpiresIn!==void 0&&{accessExpiresIn:e.accessExpiresIn},...e.refreshExpiresIn!==void 0&&{refreshExpiresIn:e.refreshExpiresIn},...e.saltRounds!==void 0&&{saltRounds:e.saltRounds},...e.apiKey!==void 0&&{apiKey:e.apiKey},...e.cookie!==void 0&&{cookie:e.cookie},...e.accessCookie!==void 0&&{accessCookie:e.accessCookie},...e.hooks!==void 0&&{hooks:e.hooks},...e.router!==void 0&&{router:e.router},...e.isTokenRevoked!==void 0&&{isTokenRevoked:e.isTokenRevoked},...e.redisUrl!==void 0&&{redisUrl:e.redisUrl},...e.logger!==void 0&&{logger:e.logger},...e.loggerService!==void 0&&{loggerService:e.loggerService}};return Le(t)}export{He as SENTRI_ERROR_STATUS,c as SentriError,Le as createAuth,Yr as createAuthServer,V as createErrorHandler,Ue as createIdempotencyMiddleware,se as getCurrentAccessToken,ne as register};
|
|
1
|
+
import je from'bcrypt';import W from'jsonwebtoken';import {randomUUID,generateKeyPairSync,createPublicKey,createPrivateKey}from'crypto';import {Kysely,sql,PostgresDialect}from'kysely';import {Router}from'express';import {Redis}from'ioredis';import {Pool}from'pg';var Fe=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}),c=class extends Error{code;statusCode;constructor(r,t,s){super(t),this.name="SentriError",this.code=r,this.statusCode=s??Fe[r]??500;}};async function G(e,r=12){return je.hash(e,r)}async function Z(e,r){return je.compare(e,r)}var qe=new Map,$e=new Map,Or=3600*1e3;function Ve(e){let r=qe.get(e);if(!r){let t=createPrivateKey(e),s=createPublicKey(t),n=s.export({format:"jwk"}),i=Buffer.from(e).slice(0,8).toString("base64url"),u={...n,use:"sig",kid:i};r={kid:i,publicKey:s,jwk:u},qe.set(e,r);}return r}function Be(e){let{jwk:r}=Ve(e);return {keys:[r]}}function Xe(e){return Ve(e).publicKey}async function Je(e){let r=Date.now(),t=$e.get(e);if(t&&r-t.fetchedAt<Or)return t.publicKey;let s=await fetch(e);if(!s.ok)throw new c("CONFIGURATION_ERROR",`Failed to fetch public key from ${e}: HTTP ${s.status}`);let n=await s.json();if(!n.keys||n.keys.length===0)throw new c("CONFIGURATION_ERROR",`No keys found in JWKS response from ${e}`);let i=n.keys[0],u=createPublicKey({key:i,format:"jwk"});return $e.set(e,{publicKey:u,fetchedAt:r}),u}var ze=new WeakMap,Ge=32,Ze=10,We=31;function Qe(e){if(e.mode==="client"){if(!e.keyUri||e.keyUri.trim().length===0)throw new c("CONFIGURATION_ERROR","keyUri must not be empty");return}if(!e.secret||e.secret.trim().length===0)throw new c("CONFIGURATION_ERROR","secret must not be empty");if((e.algorithm??"HS256").startsWith("HS")&&e.secret.length<Ge)throw new c("CONFIGURATION_ERROR",`secret must be at least ${Ge} characters for HMAC algorithms`);let s=e.saltRounds??12;if(!Number.isInteger(s)||s<Ze||s>We)throw new c("CONFIGURATION_ERROR",`saltRounds must be an integer between ${Ze} and ${We}`);if(!e.validRoles||e.validRoles.length===0)throw new c("CONFIGURATION_ERROR","validRoles must contain at least one role");if(!e.dialect)throw new c("CONFIGURATION_ERROR","dialect is required in server mode")}function C(e){let r=ze.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),cookieName:e.cookie?.name??"refresh_token",accessCookieName:e.accessCookie?.name??"access_token"};return ze.set(e,t),t}var Ur=/^(\d+)([smhdw])$/,Kr={s:1e3,m:6e4,h:36e5,d:864e5,w:6048e5},Ye=new Map;function j(e){if(typeof e=="number")return e*1e3;let r=Ye.get(e);if(r!==void 0)return r;let t=Ur.exec(e);if(!t?.[1]||!t?.[2])throw new Error(`Invalid expiresIn: "${e}". Use e.g. "15m", "7d", "1h".`);let s=Kr[t[2]];if(s===void 0)throw new Error(`Invalid expiresIn: "${e}". Use e.g. "15m", "7d", "1h".`);let n=parseInt(t[1],10)*s;return Ye.set(e,n),n}var er=new Map,rr=new Map,tr=new Map;function sr(e){return e.startsWith("RS")||e.startsWith("PS")}function ir(e){let r=er.get(e);return r||(r={access:`${e}:access`,refresh:`${e}:refresh`},er.set(e,r)),r}function Hr(e){let r=rr.get(e);return r||(r=createPrivateKey(e),rr.set(e,r)),r}function nr(e){let r=C(e);if(sr(r.algorithm)){let n=Hr(e.secret);return {accessKey:n,refreshKey:n}}let{access:t,refresh:s}=ir(e.secret);return {accessKey:t,refreshKey:s}}function or(e,r){let t=C(e);if(sr(t.algorithm))return Xe(e.secret);let{access:s,refresh:n}=ir(e.secret);return r==="access"?s:n}function ar(e,r,t,s){let n=`${t}:${s}`,i=tr.get(n);return i||(i={expiresIn:t,algorithm:s},tr.set(n,i)),W.sign(e,r,i)}function ur(e,r,t){try{let s=W.verify(e,r,{algorithms:[t]});if(typeof s=="string"||s===null)throw new c("TOKEN_INVALID","Token payload is not an object");return s}catch(s){throw s instanceof c?s:s instanceof W.TokenExpiredError?new c("TOKEN_EXPIRED","Token has expired"):new c("TOKEN_INVALID","Token is invalid or malformed")}}function Y(e,r){let t=C(r),{accessKey:s}=nr(r);return ar(e,s,t.accessExpiresIn,t.algorithm)}function Q(e,r){let t=C(r),{refreshKey:s}=nr(r);return ar({sessionId:e},s,t.refreshExpiresIn,t.algorithm)}function de(e,r){let t=C(r),s=or(r,"access");return ur(e,s,t.algorithm)}function ee(e,r){let t=C(r),s=or(r,"refresh");return ur(e,s,t.algorithm)}function dr(e,r){try{let t=W.verify(e,r,{algorithms:["RS256","RS384","RS512","PS256","PS384","PS512"]});if(typeof t=="string"||t===null)throw new c("TOKEN_INVALID","Token payload is not an object");return t}catch(t){throw t instanceof c?t:t instanceof W.TokenExpiredError?new c("TOKEN_EXPIRED","Token has expired"):new c("TOKEN_INVALID","Token is invalid or malformed")}}function F(e){return C(e).cookieName}function q(e,r){if(!e)return;let t=`${r}=`,s=0;for(;s<e.length;){for(;s<e.length&&e[s]===" ";)s++;let n=e.indexOf(";",s),i=n===-1?e.length:n;if(e.startsWith(t,s))return e.slice(s+t.length,i);s=i+1;}}function re(e,r,t){let s=t.cookie??{},n=C(t),i=j(n.refreshExpiresIn);e.cookie(F(t),r,{httpOnly:s.httpOnly??true,secure:s.secure??false,sameSite:s.sameSite??"strict",path:s.path??"/",maxAge:i});}function ce(e,r){let t=r.cookie??{};e.clearCookie(F(r),{path:t.path??"/"});}function Se(e){return C(e).accessCookieName}function te(e,r,t){if(!t.accessCookie)return;let s=t.accessCookie,n=C(t),i=j(n.accessExpiresIn);e.cookie(Se(t),r,{httpOnly:false,secure:s.secure??false,sameSite:s.sameSite??"strict",path:s.path??"/",maxAge:i});}function Ee(e,r){if(!r.accessCookie)return;let t=r.accessCookie;e.clearCookie(Se(r),{path:t.path??"/"});}function se(e,r){let t=e.headers.authorization;return t?.startsWith("Bearer ")?t.slice(7):q(e.headers.cookie,Se(r))}var cr=new Map;function k(e){let r=cr.get(e);return r||(r=new Kysely({dialect:e}),cr.set(e,r)),r}function be(e){try{return JSON.parse(e)}catch{return []}}function jr(e){return JSON.stringify(e)}function fr(e){return {id:e.id,userId:e.user_id,type:e.type,value:e.value,isPrimary:e.is_primary===1,createdAt:new Date(e.created_at)}}async function ie(e,r){let t=await e.selectFrom("sentri_identifiers as i_login").innerJoin("sentri_users as u","u.id","i_login.user_id").innerJoin("sentri_identifiers as i_primary",s=>s.onRef("i_primary.user_id","=","u.id").on("i_primary.is_primary","=",1)).select(["u.id","u.password_hash","u.roles","i_primary.value as primary_value","i_primary.type as primary_type"]).where("i_login.value","=",r).executeTakeFirst();return t?{id:t.id,identifier:t.primary_value,identifierType:t.primary_type,passwordHash:t.password_hash,roles:be(t.roles)}:null}async function le(e,r){let t=await e.selectFrom("sentri_users as u").innerJoin("sentri_identifiers as i",s=>s.onRef("i.user_id","=","u.id").on("i.is_primary","=",1)).select(["u.id","u.password_hash","u.roles","i.value as primary_value","i.type as primary_type"]).where("u.id","=",r).executeTakeFirst();return t?{id:t.id,identifier:t.primary_value,identifierType:t.primary_type,passwordHash:t.password_hash,roles:be(t.roles)}:null}async function pr(e,r,t){let s=t.map(n=>({id:randomUUID(),user_id:r,type:n.type,value:n.value,is_primary:n.isPrimary?1:0}));return await e.insertInto("sentri_identifiers").values(s).execute(),s.map(n=>({id:n.id,userId:n.user_id,type:n.type,value:n.value,isPrimary:n.is_primary===1,createdAt:new Date}))}async function $(e,r){return (await e.selectFrom("sentri_identifiers").selectAll().where("user_id","=",r).orderBy("created_at","asc").execute()).map(fr)}async function fe(e,r,t){let s=await e.selectFrom("sentri_identifiers").selectAll().where("id","=",r).where("user_id","=",t).executeTakeFirst();return s?fr(s):null}async function mr(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)}async function gr(e,r,t,s){await e.updateTable("sentri_identifiers").set({type:s.type,value:s.value}).where("id","=",r).where("user_id","=",t).execute();}async function hr(e,r,t){await e.deleteFrom("sentri_identifiers").where("user_id","=",r).where("id","in",t).execute();}async function yr(e,r,t){await e.transaction().execute(async s=>{await s.updateTable("sentri_identifiers").set({is_primary:0}).where("user_id","=",r).execute(),await s.updateTable("sentri_identifiers").set({is_primary:1}).where("id","=",t).where("user_id","=",r).execute();});}async function Rr(e,r,t){await e.updateTable("sentri_users").set({password_hash:t}).where("id","=",r).execute();}async function wr(e,r,t){await e.updateTable("sentri_users").set({roles:jr(t)}).where("id","=",r).execute();}async function xe(e,r){let t=randomUUID();return await e.insertInto("sentri_sessions").values({id:t,user_id:r.userId,expires_at:r.expiresAt}).execute(),{id:t}}async function Ir(e,r){let t=await e.selectFrom("sentri_sessions as s").innerJoin("sentri_users as u","u.id","s.user_id").innerJoin("sentri_identifiers as i",s=>s.onRef("i.user_id","=","u.id").on("i.is_primary","=",1)).select(["s.id as session_id","s.user_id","s.expires_at","s.created_at as session_created_at","u.id as user_id_col","u.password_hash","u.roles","i.value as primary_identifier","i.type as primary_identifier_type"]).where("s.id","=",r).executeTakeFirst();return t?{id:t.session_id,userId:t.user_id,expiresAt:new Date(t.expires_at),createdAt:new Date(t.session_created_at),user:{id:t.user_id_col,identifier:t.primary_identifier,identifierType:t.primary_identifier_type,passwordHash:t.password_hash,roles:be(t.roles)}}:null}async function pe(e,r){await e.deleteFrom("sentri_sessions").where("id","=",r).execute();}async function De(e,r){await e.deleteFrom("sentri_sessions").where("user_id","=",r).execute();}async function ne(e,r){let t=C(r),s=k(r.dialect),n=e.roles??[],i=n.filter(_=>!t.validRolesSet.has(_));if(i.length>0)return {success:false,error:new c("INVALID_ROLE",`Invalid roles: ${i.join(", ")}`)};if(!e.identifiers||e.identifiers.length===0)return {success:false,error:new c("VALIDATION_ERROR","At least one identifier is required")};let u=e.identifiers.map(_=>({type:_.type.trim(),value:_.value.trim()}));if(new Set(u.map(_=>_.value)).size!==u.length)return {success:false,error:new c("VALIDATION_ERROR","Duplicate identifier values in request")};for(let _ of u)if(await ie(s,_.value))return {success:false,error:new c("IDENTIFIER_ALREADY_EXISTS",`Identifier already taken: ${_.value}`)};let d=await G(e.password,t.saltRounds),{userId:g,identifierRows:y}=await s.transaction().execute(async _=>{let N=randomUUID();await _.insertInto("sentri_users").values({id:N,password_hash:d,roles:JSON.stringify(n)}).execute();let K=u.map((L,ke)=>({id:randomUUID(),user_id:N,type:L.type,value:L.value,is_primary:ke===0?1:0}));return await _.insertInto("sentri_identifiers").values(K).execute(),{userId:N,identifierRows:K}}),x=y.map(_=>({id:_.id,type:_.type,value:_.value,isPrimary:_.is_primary===1})),S=x[0];return {success:true,user:{id:g,identifier:S.value,identifierType:S.type,roles:n,identifiers:x}}}async function me(e,r){let t=C(r),s=k(r.dialect),n=await ie(s,e.identifier.trim());if(!n)return {success:false,error:new c("INVALID_CREDENTIALS","Invalid credentials")};if(!await Z(e.password,n.passwordHash))return {success:false,error:new c("INVALID_CREDENTIALS","Invalid credentials")};let u=new Date(Date.now()+j(t.refreshExpiresIn)),a=await xe(s,{userId:n.id,expiresAt:u}),d={id:n.id,identifier:n.identifier,identifierType:n.identifierType,roles:n.roles},g=Y({id:n.id,identifier:n.identifier,identifierType:n.identifierType,roles:n.roles,sessionId:a.id},r),y=Q(a.id,r);return {success:true,accessToken:g,refreshToken:y,user:d}}async function M(e,r){let t=C(r),s=k(r.dialect),n;try{({sessionId:n}=ee(e,r));}catch(x){return x instanceof c?{success:false,error:x}:{success:false,error:new c("TOKEN_INVALID","Invalid refresh token")}}let i=await Ir(s,n);if(!i)return {success:false,error:new c("UNAUTHORIZED","Session not found or revoked")};if(i.expiresAt.getTime()<Date.now())return await pe(s,n),{success:false,error:new c("TOKEN_EXPIRED","Session has expired")};await pe(s,n);let u=new Date(Date.now()+j(t.refreshExpiresIn)),a=await xe(s,{userId:i.userId,expiresAt:u}),d={id:i.user.id,identifier:i.user.identifier,identifierType:i.user.identifierType,roles:i.user.roles},g=Y({...d,sessionId:a.id},r),y=Q(a.id,r);return {success:true,accessToken:g,refreshToken:y,user:d}}async function ge(e,r){let t=k(r.dialect),s;try{({sessionId:s}=ee(e,r));}catch{return}await pe(t,s);}async function he(e,r){let t=k(r.dialect);await De(t,e);}async function ye(e,r){let t=k(r.dialect),s=await le(t,e);if(!s)return {success:false,error:new c("USER_NOT_FOUND","User not found")};let n=await $(t,e);return {success:true,user:{id:s.id,identifier:s.identifier,identifierType:s.identifierType,roles:s.roles,identifiers:n.map(i=>({id:i.id,type:i.type,value:i.value,isPrimary:i.isPrimary}))}}}async function Re(e,r,t,s){let n=C(s),i=k(s.dialect),u=await le(i,e);if(!u)return {success:false,error:new c("USER_NOT_FOUND","User not found")};if(!await Z(r,u.passwordHash))return {success:false,error:new c("INVALID_CREDENTIALS","Invalid credentials")};let d=await G(t,n.saltRounds);return await Rr(i,e,d),await De(i,e),{success:true}}async function we(e,r,t){let s=C(t),n=k(t.dialect),i=r.filter(g=>!s.validRolesSet.has(g));if(i.length>0)return {success:false,error:new c("INVALID_ROLE",`Invalid roles: ${i.join(", ")}`)};let u=await le(n,e);if(!u)return {success:false,error:new c("USER_NOT_FOUND","User not found")};let a=new Set(u.roles);for(let g of r)a.add(g);let d=Array.from(a);return await wr(n,e,d),{success:true,user:{id:u.id,identifier:u.identifier,identifierType:u.identifierType,roles:d}}}async function Ie(e,r,t){let s=k(t.dialect);if(r.length===0)return {success:false,error:new c("VALIDATION_ERROR","At least one identifier is required")};let n=r.map(d=>({type:d.type.trim(),value:d.value.trim()}));if(new Set(n.map(d=>d.value)).size!==n.length)return {success:false,error:new c("VALIDATION_ERROR","Duplicate identifier values in request")};for(let d of n)if(await ie(s,d.value))return {success:false,error:new c("IDENTIFIER_ALREADY_EXISTS",`Identifier already taken: ${d.value}`)};await pr(s,e,n.map(d=>({...d,isPrimary:false})));return {success:true,identifiers:(await $(s,e)).map(d=>({id:d.id,type:d.type,value:d.value,isPrimary:d.isPrimary}))}}async function Ae(e,r,t){let s=k(t.dialect);if(r.length===0)return {success:false,error:new c("VALIDATION_ERROR","At least one update is required")};let n=r.map(a=>({id:a.id,type:a.type.trim(),value:a.value.trim()}));if(new Set(n.map(a=>a.value)).size!==n.length)return {success:false,error:new c("VALIDATION_ERROR","Duplicate identifier values in request")};for(let a of n){let d=await fe(s,a.id,e);if(!d)return {success:false,error:new c("IDENTIFIER_NOT_FOUND",`Identifier not found: ${a.id}`)};if(d.value!==a.value&&await ie(s,a.value))return {success:false,error:new c("IDENTIFIER_ALREADY_EXISTS",`Identifier already taken: ${a.value}`)}}for(let a of n)await gr(s,a.id,e,{type:a.type,value:a.value});return {success:true,identifiers:(await $(s,e)).map(a=>({id:a.id,type:a.type,value:a.value,isPrimary:a.isPrimary}))}}async function ve(e,r,t){let s=k(t.dialect);if(r.length===0)return {success:false,error:new c("VALIDATION_ERROR","At least one ID is required")};for(let u of r)if(!await fe(s,u,e))return {success:false,error:new c("IDENTIFIER_NOT_FOUND",`Identifier not found: ${u}`)};return await mr(s,e)-r.length<1?{success:false,error:new c("VALIDATION_ERROR","Cannot delete all identifiers \u2014 at least one must remain")}:(await hr(s,e,r),{success:true,identifiers:(await $(s,e)).map(u=>({id:u.id,type:u.type,value:u.value,isPrimary:u.isPrimary}))})}async function _e(e,r,t){let s=k(t.dialect);return await fe(s,r,e)?(await yr(s,e,r),{success:true,identifiers:(await $(s,e)).map(u=>({id:u.id,type:u.type,value:u.value,isPrimary:u.isPrimary}))}):{success:false,error:new c("IDENTIFIER_NOT_FOUND",`Identifier not found: ${r}`)}}var O={info:()=>{},warn:()=>{},error:()=>{}};function m(e,r,t){return {service:e,event:r,...t}}function E(e){let r=e.logger??O,t=e.loggerService??"sentri";return e.mode==="client"?qr(e.keyUri,r,t):$r(e,r,t)}function qr(e,r,t){return async(s,n,i)=>{let u=s.headers.authorization,a=u?.startsWith("Bearer ")?u.slice(7):void 0,d=s.requestId;if(!a)return r.warn(m(t,"auth.protect.failure",{mode:"client",errorCode:"UNAUTHORIZED",...d!==void 0&&{requestId:d}})),i(new c("UNAUTHORIZED","Missing or malformed Authorization header"));try{let g=await Je(e),y=dr(a,g);s.user={id:y.id,identifier:y.identifier,identifierType:y.identifierType,roles:y.roles},r.info(m(t,"auth.protect.success",{mode:"client",userId:y.id,...d!==void 0&&{requestId:d}})),i();}catch(g){let y=g instanceof c?g.code:"TOKEN_INVALID";r.warn(m(t,"auth.protect.failure",{mode:"client",errorCode:y,...d!==void 0&&{requestId:d}})),i(g);}}}function $r(e,r,t){return async(s,n,i)=>{let u=se(s,e),a=s.requestId;if(!u)return r.warn(m(t,"auth.protect.failure",{mode:"server",errorCode:"UNAUTHORIZED",...a!==void 0&&{requestId:a}})),i(new c("UNAUTHORIZED","Missing or malformed Authorization header"));try{let d=de(u,e);if(e.isTokenRevoked&&await e.isTokenRevoked(d.sessionId))return r.warn(m(t,"auth.protect.token_revoked",{mode:"server",userId:d.id,...a!==void 0&&{requestId:a}})),i(new c("UNAUTHORIZED","Token has been revoked"));s.user={id:d.id,identifier:d.identifier,identifierType:d.identifierType,roles:d.roles},r.info(m(t,"auth.protect.success",{mode:"server",userId:d.id,...a!==void 0&&{requestId:a}})),i();}catch(d){if(d instanceof c&&d.code==="TOKEN_EXPIRED"){let g=q(s.headers.cookie,F(e));if(!g)return r.warn(m(t,"auth.protect.failure",{mode:"server",errorCode:"TOKEN_EXPIRED",...a!==void 0&&{requestId:a}})),i(new c("UNAUTHORIZED","Token expired. Please login again."));try{let y=await M(g,e);if(!y.success)return r.warn(m(t,"auth.protect.failure",{mode:"server",errorCode:y.error.code,...a!==void 0&&{requestId:a}})),i(new c("UNAUTHORIZED","Session expired. Please login again."));re(n,y.refreshToken,e),te(n,y.accessToken,e),n.setHeader("X-New-Access-Token",y.accessToken),s.user=y.user,r.info(m(t,"auth.protect.auto_refresh",{mode:"server",userId:y.user.id,...a!==void 0&&{requestId:a}})),i();}catch{r.warn(m(t,"auth.protect.failure",{mode:"server",errorCode:"TOKEN_EXPIRED",...a!==void 0&&{requestId:a}})),i(new c("UNAUTHORIZED","Session expired. Please login again."));}}else {let g=d instanceof c?d.code:"TOKEN_INVALID";r.warn(m(t,"auth.protect.failure",{mode:"server",errorCode:g,...a!==void 0&&{requestId:a}})),i(d);}}}}function vr(e,r,t){let s=`Requires one of roles: ${e.join(", ")}`;return (n,i,u)=>{let a=n.requestId;if(!n.user)return r.warn(m(t,"auth.authorize.unauthenticated",{requiredRoles:e,...a!==void 0&&{requestId:a}})),u(new c("UNAUTHORIZED","Not authenticated"));let d=n.user.roles;if(!e.some(g=>d.includes(g)))return r.warn(m(t,"auth.authorize.denied",{userId:n.user.id,userRoles:[...d],requiredRoles:e,...a!==void 0&&{requestId:a}})),u(new c("FORBIDDEN",s));r.info(m(t,"auth.authorize.passed",{userId:n.user.id,userRoles:[...d],requiredRoles:e,...a!==void 0&&{requestId:a}})),u();}}function oe(e,r){return function(...s){return vr(s,e,r)}}function Pe(...e){return vr(e,O,"sentri")}var Mr=new c("FORBIDDEN","You do not have permission to perform this action");function _r(e,r,t){return async(s,n,i)=>{let u=s.requestId;if(!s.user)return r.warn(m(t,"auth.permit.unauthenticated",{...u!==void 0&&{requestId:u}})),i(new c("UNAUTHORIZED","Not authenticated"));let a=s.user.id;if(e.roles&&e.roles.length>0){let d=s.user.roles;if(e.roles.some(g=>d.includes(g)))return r.info(m(t,"auth.permit.role_bypass",{userId:a,bypassedByRole:true,...u!==void 0&&{requestId:u}})),i()}try{let d=e.check(s);(d instanceof Promise?await d:d)?(r.info(m(t,"auth.permit.passed",{userId:a,...u!==void 0&&{requestId:u}})),i()):(r.warn(m(t,"auth.permit.denied",{userId:a,...u!==void 0&&{requestId:u}})),i(Mr));}catch(d){i(d);}}}function ae(e,r){return function(s){return _r(typeof s=="function"?{check:s}:s,e,r)}}function Ne(e){return _r(typeof e=="function"?{check:e}:e,O,"sentri")}function V(e){return (r,t,s,n)=>{if(r instanceof c){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});}}var Te=8,B=72,Ce=255,Tr=100,X=50;function I(e){return new c("VALIDATION_ERROR",e)}function b(e,r,t,s){e.status(r).json({error:false,statusCode:r,message:t,data:s});}function D(e,r){e.status(r.statusCode).json({error:true,statusCode:r.statusCode,code:r.code,message:r.message,data:null});}function U(e){if(e==null||typeof e!="object"||Array.isArray(e))throw new c("VALIDATION_ERROR","Request body is missing or not a JSON object. Did you apply express.json()?");return e}function Br(e,r){if(!r.apiKey)return;let t=e.headers["x-api-key"];if(typeof t!="string"||t!==r.apiKey)throw new c("UNAUTHORIZED","Invalid or missing API key")}function Oe(e){if(e)try{let r=e();r instanceof Promise&&r.catch(()=>{});}catch{}}function Xr(e){return e.startsWith("RS")||e.startsWith("PS")}function Ue(e,r){if(typeof e!="object"||e===null||Array.isArray(e))throw I(`identifiers[${r}] must be an object`);let t=e;if(typeof t.type!="string"||t.type.trim().length===0)throw I(`identifiers[${r}].type is required and must be a non-empty string`);if(t.type.length>Tr)throw I(`identifiers[${r}].type must not exceed ${Tr} characters`);if(typeof t.value!="string"||t.value.trim().length===0)throw I(`identifiers[${r}].value is required and must be a non-empty string`);if(t.value.length>Ce)throw I(`identifiers[${r}].value must not exceed ${Ce} characters`);return {type:t.type,value:t.value}}function T(e){return e.requestId!==void 0?{requestId:e.requestId}:{}}function Cr(e){let r=Router(),t=e,s=C(t),n=e.logger??O,i=e.loggerService??"sentri",u=oe(n,i),a=ae(n,i),d=e.router?.register??(o=>ne(o,t)),g=e.router?.login??(o=>me(o,t)),y=e.router?.refresh??(o=>M(o,t)),x=e.router?.logout??(o=>o!==void 0?ge(o,t):Promise.resolve()),S=e.router?.logoutAll??(o=>he(o,t)),_=e.router?.getUser??(o=>ye(o,t)),N=e.router?.assignRoles??((o,f)=>we(o,f,t)),K=e.router?.changePassword??((o,f,A)=>Re(o,f,A,t)),L=e.router?.bulkCreateIdentifiers??((o,f)=>Ie(o,f,t)),ke=e.router?.bulkUpdateIdentifiers??((o,f)=>Ae(o,f,t)),xr=e.router?.bulkDeleteIdentifiers??((o,f)=>ve(o,f,t)),Dr=e.router?.changePrimaryIdentifier??((o,f)=>_e(o,f,t));Xr(s.algorithm)&&r.get("/keys",(o,f)=>{f.setHeader("Cache-Control","public, max-age=3600"),f.json(Be(e.secret));}),r.post("/register",async(o,f,A)=>{let h=Date.now();try{Br(o,e);let l=U(o.body),{identifiers:p,password:R,roles:w}=l;if(!Array.isArray(p)||p.length===0)throw I("identifiers is required and must be a non-empty array");if(p.length>X)throw I(`identifiers must not exceed ${X} entries`);let v=p.map((z,Pr)=>Ue(z,Pr));if(typeof R!="string"||R.length<Te)throw I(`password is required and must be at least ${Te} characters`);if(R.length>B)throw I(`password must not exceed ${B} characters`);if(w!==void 0&&!Array.isArray(w))throw I("roles must be an array of strings when provided");if(Array.isArray(w)&&!w.every(z=>typeof z=="string"))throw I("each role must be a string");let P=Array.isArray(w)?w:void 0,H=await d(P!==void 0?{identifiers:v,password:R,roles:P}:{identifiers:v,password:R});if(!H.success){n.warn(m(i,"auth.register.failure",{errorCode:H.error.code,duration_ms:Date.now()-h,...T(o)})),D(f,H.error);return}n.info(m(i,"auth.register.success",{userId:H.user.id,duration_ms:Date.now()-h,...T(o)})),b(f,201,"User registered successfully",{user:H.user});}catch(l){A(l);}}),r.post("/login",async(o,f,A)=>{let h=Date.now();try{let l=U(o.body),{identifier:p,password:R}=l;if(typeof p!="string"||p.trim().length===0)throw I("identifier is required and must be a non-empty string");if(p.length>Ce)throw I(`identifier must not exceed ${Ce} characters`);if(typeof R!="string"||R.length===0)throw I("password is required");if(R.length>B)throw I(`password must not exceed ${B} characters`);let w=p.trim(),v=await g({identifier:w,password:R});if(!v.success){Oe(()=>e.hooks?.onFailedLogin?.(w,v.error)),n.warn(m(i,"auth.login.failure",{errorCode:v.error.code,duration_ms:Date.now()-h,...T(o)})),D(f,v.error);return}Oe(()=>e.hooks?.onLogin?.(v.user)),re(f,v.refreshToken,e),te(f,v.accessToken,e),n.info(m(i,"auth.login.success",{userId:v.user.id,duration_ms:Date.now()-h,...T(o)})),b(f,200,"Login successful",{accessToken:v.accessToken,user:v.user});}catch(l){A(l);}}),r.post("/refresh",async(o,f,A)=>{let h=Date.now();try{let l=q(o.headers.cookie,F(e));if(!l)throw new c("UNAUTHORIZED","Refresh token cookie is missing");let p=await y(l);if(!p.success){ce(f,e),n.warn(m(i,"auth.refresh.failure",{errorCode:p.error.code,duration_ms:Date.now()-h,...T(o)})),D(f,p.error);return}re(f,p.refreshToken,e),te(f,p.accessToken,e),n.info(m(i,"auth.refresh.success",{userId:p.user.id,duration_ms:Date.now()-h,...T(o)})),b(f,200,"Token refreshed",{accessToken:p.accessToken});}catch(l){A(l);}}),r.post("/logout",async(o,f,A)=>{let h=Date.now();try{let l=q(o.headers.cookie,F(e));await x(l),ce(f,e),Ee(f,e),n.info(m(i,"auth.logout",{duration_ms:Date.now()-h,...T(o)})),b(f,200,"Logged out",null);}catch(l){A(l);}}),r.post("/logout-all",E(e),async(o,f,A)=>{let h=Date.now();try{let l=o.user.id;await S(l),Oe(()=>e.hooks?.onLogout?.(l)),ce(f,e),Ee(f,e),n.info(m(i,"auth.logout_all",{userId:l,duration_ms:Date.now()-h,...T(o)})),b(f,200,"All sessions revoked",null);}catch(l){A(l);}}),r.get("/me",E(e),async(o,f,A)=>{let h=Date.now();try{let l=await _(o.user.id);if(!l.success){n.warn(m(i,"auth.me.failure",{userId:o.user.id,errorCode:l.error.code,duration_ms:Date.now()-h,...T(o)})),D(f,l.error);return}n.info(m(i,"auth.me.success",{userId:l.user.id,duration_ms:Date.now()-h,...T(o)})),b(f,200,"OK",l.user);}catch(l){A(l);}}),r.get("/me/identifiers",E(e),async(o,f,A)=>{let h=Date.now();try{let l=await _(o.user.id);if(!l.success){n.warn(m(i,"auth.me.identifiers.failure",{userId:o.user.id,errorCode:l.error.code,duration_ms:Date.now()-h,...T(o)})),D(f,l.error);return}n.info(m(i,"auth.me.identifiers.success",{userId:l.user.id,count:l.user.identifiers?.length??0,duration_ms:Date.now()-h,...T(o)})),b(f,200,"OK",{identifiers:l.user.identifiers??[]});}catch(l){A(l);}});let J=a(o=>!!o.user);return r.post("/me/identifiers",E(e),J,async(o,f,A)=>{let h=Date.now();try{let l=U(o.body),{identifiers:p}=l;if(!Array.isArray(p)||p.length===0)throw I("identifiers is required and must be a non-empty array");if(p.length>X)throw I(`identifiers must not exceed ${X} entries`);let R=p.map((v,P)=>Ue(v,P)),w=await L(o.user.id,R);if(!w.success){n.warn(m(i,"auth.identifiers.create_failure",{userId:o.user.id,errorCode:w.error.code,duration_ms:Date.now()-h,...T(o)})),D(f,w.error);return}n.info(m(i,"auth.identifiers.created",{userId:o.user.id,count:w.identifiers.length,duration_ms:Date.now()-h,...T(o)})),b(f,201,"Identifiers added successfully",{identifiers:w.identifiers});}catch(l){A(l);}}),r.put("/me/identifiers",E(e),J,async(o,f,A)=>{let h=Date.now();try{let l=U(o.body),{identifiers:p}=l;if(!Array.isArray(p)||p.length===0)throw I("identifiers is required and must be a non-empty array");if(p.length>X)throw I(`identifiers must not exceed ${X} entries`);let R=p.map((v,P)=>{if(typeof v!="object"||v===null||Array.isArray(v))throw I(`identifiers[${P}] must be an object`);let ue=v;if(typeof ue.id!="string"||ue.id.trim().length===0)throw I(`identifiers[${P}].id is required and must be a non-empty string`);let{type:H,value:z}=Ue(v,P);return {id:ue.id,type:H,value:z}}),w=await ke(o.user.id,R);if(!w.success){n.warn(m(i,"auth.identifiers.update_failure",{userId:o.user.id,errorCode:w.error.code,duration_ms:Date.now()-h,...T(o)})),D(f,w.error);return}n.info(m(i,"auth.identifiers.updated",{userId:o.user.id,count:w.identifiers.length,duration_ms:Date.now()-h,...T(o)})),b(f,200,"Identifiers updated successfully",{identifiers:w.identifiers});}catch(l){A(l);}}),r.delete("/me/identifiers",E(e),J,async(o,f,A)=>{let h=Date.now();try{let l=U(o.body),{ids:p}=l;if(!Array.isArray(p)||p.length===0)throw I("ids is required and must be a non-empty array of strings");if(!p.every(w=>typeof w=="string"))throw I("each id must be a string");let R=await xr(o.user.id,p);if(!R.success){n.warn(m(i,"auth.identifiers.delete_failure",{userId:o.user.id,errorCode:R.error.code,duration_ms:Date.now()-h,...T(o)})),D(f,R.error);return}n.info(m(i,"auth.identifiers.deleted",{userId:o.user.id,duration_ms:Date.now()-h,...T(o)})),b(f,200,"Identifiers deleted successfully",{identifiers:R.identifiers});}catch(l){A(l);}}),r.patch("/me/identifiers/primary",E(e),J,async(o,f,A)=>{let h=Date.now();try{let l=U(o.body),{id:p}=l;if(typeof p!="string"||p.trim().length===0)throw I("id is required and must be a non-empty string");let R=await Dr(o.user.id,p.trim());if(!R.success){n.warn(m(i,"auth.identifiers.primary_change_failure",{userId:o.user.id,errorCode:R.error.code,duration_ms:Date.now()-h,...T(o)})),D(f,R.error);return}n.info(m(i,"auth.identifiers.primary_changed",{userId:o.user.id,duration_ms:Date.now()-h,...T(o)})),b(f,200,"Primary identifier updated successfully",{identifiers:R.identifiers});}catch(l){A(l);}}),r.patch("/me/password",E(e),J,async(o,f,A)=>{let h=Date.now();try{let l=U(o.body),{currentPassword:p,newPassword:R}=l;if(typeof p!="string"||p.length===0)throw I("currentPassword is required");if(typeof R!="string"||R.length<Te)throw I(`newPassword must be at least ${Te} characters`);if(R.length>B)throw I(`newPassword must not exceed ${B} characters`);if(p===R)throw I("newPassword must be different from currentPassword");let w=await K(o.user.id,p,R);if(!w.success){n.warn(m(i,"auth.password.change_failure",{userId:o.user.id,errorCode:w.error.code,duration_ms:Date.now()-h,...T(o)})),D(f,w.error);return}n.info(m(i,"auth.password.changed",{userId:o.user.id,duration_ms:Date.now()-h,...T(o)})),b(f,200,"Password updated successfully. All sessions have been revoked.",null);}catch(l){A(l);}}),r.post("/users/:userId/roles",E(e),u("admin"),async(o,f,A)=>{let h=Date.now();try{let l=U(o.body),{roles:p}=l,R=o.params.userId,w=typeof R=="string"?R:void 0;if(!w)throw I("userId is required");if(!Array.isArray(p)||p.length===0)throw I("roles must be a non-empty array of strings");if(!p.every(P=>typeof P=="string"))throw I("each role must be a string");let v=await N(w,p);if(!v.success){n.warn(m(i,"auth.roles.assign_failure",{targetUserId:w,errorCode:v.error.code,duration_ms:Date.now()-h,...T(o)})),D(f,v.error);return}n.info(m(i,"auth.roles.assigned",{targetUserId:w,roles:p,duration_ms:Date.now()-h,...T(o)})),b(f,200,"Roles assigned successfully",{user:v.user});}catch(l){A(l);}}),r.use(V()),r}var kr=new Map;function Sr(e){let r=kr.get(e);return r||(r=new Redis(e,{lazyConnect:false,enableOfflineQueue:false}),kr.set(e,r)),r}function Ke(e){let r=e?.ttl??3e5,t=(e?.header??"X-Idempotency-Key").toLowerCase(),s=new Set((e?.methods??["POST","PUT","PATCH"]).map(i=>i.toUpperCase())),n=e?.redisUrl;return n?zr(n,r,t,s):Gr(r,t,s,e?.maxSize??1e4)}function zr(e,r,t,s){let n=Sr(e),i="sentri:idempotency:";return async(u,a,d)=>{let g=u.headers[t];if(!g||typeof g!="string"||!s.has(u.method))return d();u.requestId=g,a.setHeader("X-Request-Id",g);let y=await n.get(`${i}${g}`);if(y){let S=JSON.parse(y);return a.setHeader("X-Idempotent-Replayed","true"),a.status(S.statusCode).json(S.body)}let x=a.json.bind(a);a.json=function(_){if(a.statusCode>=200&&a.statusCode<300){let N={statusCode:a.statusCode,body:_,expiresAt:Date.now()+r};n.set(`${i}${g}`,JSON.stringify(N),"PX",r).catch(()=>{});}return x(_)},d();}}function Gr(e,r,t,s){let n=Math.max(e,5e3),i=new Map,u=setInterval(()=>{let a=Date.now();for(let[d,g]of i)g.expiresAt<=a&&i.delete(d);},n);return typeof u=="object"&&u!==null&&"unref"in u&&u.unref(),(a,d,g)=>{let y=a.headers[r];if(!y||typeof y!="string"||!t.has(a.method))return g();a.requestId=y,d.setHeader("X-Request-Id",y);let x=Date.now(),S=i.get(y);if(S&&S.expiresAt>x)return d.setHeader("X-Idempotent-Replayed","true"),d.status(S.statusCode).json(S.body);let _=d.json.bind(d);d.json=function(K){if(d.statusCode>=200&&d.statusCode<300){if(i.size>=s){let L=i.keys().next().value;L!==void 0&&i.delete(L);}i.set(y,{statusCode:d.statusCode,body:K,expiresAt:Date.now()+e});}return _(K)},g();}}async function Er(e){await e.schema.createTable("sentri_users").ifNotExists().addColumn("id","varchar(36)",r=>r.primaryKey().notNull()).addColumn("password_hash","varchar(255)",r=>r.notNull()).addColumn("roles","text",r=>r.notNull().defaultTo("[]")).addColumn("created_at","timestamp",r=>r.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)).execute(),await e.schema.createTable("sentri_sessions").ifNotExists().addColumn("id","varchar(36)",r=>r.primaryKey().notNull()).addColumn("user_id","varchar(36)",r=>r.notNull().references("sentri_users.id").onDelete("cascade")).addColumn("expires_at","timestamp",r=>r.notNull()).addColumn("created_at","timestamp",r=>r.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)).execute(),await e.schema.createTable("sentri_identifiers").ifNotExists().addColumn("id","varchar(36)",r=>r.primaryKey().notNull()).addColumn("user_id","varchar(36)",r=>r.notNull().references("sentri_users.id").onDelete("cascade")).addColumn("type","varchar(100)",r=>r.notNull()).addColumn("value","varchar(255)",r=>r.notNull().unique()).addColumn("is_primary","integer",r=>r.notNull().defaultTo(0)).addColumn("created_at","timestamp",r=>r.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)).execute();}function He(e){if(Qe(e),e.mode==="client"){let i=e.logger?oe(e.logger,e.loggerService??"sentri"):Pe,u=e.logger?ae(e.logger,e.loggerService??"sentri"):Ne;return {protect:()=>E(e),authorize:(...a)=>i(...a),permit:a=>u(a),errorHandler:a=>V(a)}}let r=e,t=C(r),s=r.logger?oe(r.logger,r.loggerService??"sentri"):Pe,n=r.logger?ae(r.logger,r.loggerService??"sentri"):Ne;return {protect:()=>E(r),authorize:(...i)=>s(...i),permit:i=>n(i),hashPassword:i=>G(i,t.saltRounds),verifyPassword:(i,u)=>Z(i,u),signAccessToken:i=>Y(i,r),signRefreshToken:i=>Q(i,r),verifyAccessToken:i=>de(i,r),verifyRefreshToken:i=>ee(i,r),getCurrentAccessToken:i=>se(i,r),router:()=>Cr(r),migrate:()=>Er(k(r.dialect)),errorHandler:i=>V(i),idempotencyMiddleware:i=>Ke({...i,...r.redisUrl!==void 0&&{redisUrl:r.redisUrl}}),register:i=>ne(i,r),login:i=>me(i,r),refresh:i=>M(i,r),logout:i=>ge(i,r),logoutAll:i=>he(i,r),getUser:i=>ye(i,r),changePassword:(i,u,a)=>Re(i,u,a,r),assignRoles:(i,u)=>we(i,u,r),bulkCreateIdentifiers:(i,u)=>Ie(i,u,r),bulkUpdateIdentifiers:(i,u)=>Ae(i,u,r),bulkDeleteIdentifiers:(i,u)=>ve(i,u,r),changePrimaryIdentifier:(i,u)=>_e(i,u,r)}}function br(e){return new PostgresDialect({pool:new Pool(e)})}function Qr(e){let{privateKey:r}=generateKeyPairSync("rsa",{modulusLength:2048,privateKeyEncoding:{type:"pkcs8",format:"pem"},publicKeyEncoding:{type:"spki",format:"pem"}}),t={mode:"server",dialect:br(e.db),secret:r,algorithm:"RS256",validRoles:e.validRoles,...e.accessExpiresIn!==void 0&&{accessExpiresIn:e.accessExpiresIn},...e.refreshExpiresIn!==void 0&&{refreshExpiresIn:e.refreshExpiresIn},...e.saltRounds!==void 0&&{saltRounds:e.saltRounds},...e.apiKey!==void 0&&{apiKey:e.apiKey},...e.cookie!==void 0&&{cookie:e.cookie},...e.accessCookie!==void 0&&{accessCookie:e.accessCookie},...e.hooks!==void 0&&{hooks:e.hooks},...e.router!==void 0&&{router:e.router},...e.isTokenRevoked!==void 0&&{isTokenRevoked:e.isTokenRevoked},...e.redisUrl!==void 0&&{redisUrl:e.redisUrl},...e.logger!==void 0&&{logger:e.logger},...e.loggerService!==void 0&&{loggerService:e.loggerService}};return He(t)}export{Fe as SENTRI_ERROR_STATUS,c as SentriError,He as createAuth,Qr as createAuthServer,V as createErrorHandler,Ke as createIdempotencyMiddleware,se as getCurrentAccessToken,ne as register};
|