sentri 2.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import W from'bcrypt';import K from'jsonwebtoken';import {Router}from'express';var Y=Object.assign(Object.create(null),{UNAUTHORIZED:401,TOKEN_EXPIRED:401,TOKEN_INVALID:401,INVALID_CREDENTIALS:401,FORBIDDEN:403,USER_NOT_FOUND:404,USER_ALREADY_EXISTS:409,INVALID_ROLE:400,VALIDATION_ERROR:400,CONFIGURATION_ERROR:500}),i=class extends Error{code;statusCode;constructor(r,t,s){super(t),this.name="SentriError",this.code=r,this.statusCode=s??Y[r]??500;}};async function D(e,r=12){return W.hash(e,r)}async function H(e,r){return W.compare(e,r)}var J=new WeakMap,Q=32,ee=10,re=31;function se(e){if(!e.secret||e.secret.trim().length===0)throw new i("CONFIGURATION_ERROR","secret must not be empty");if(e.secret.length<Q)throw new i("CONFIGURATION_ERROR",`secret must be at least ${Q} characters to be cryptographically safe`);let r=e.saltRounds??12;if(!Number.isInteger(r)||r<ee||r>re)throw new i("CONFIGURATION_ERROR",`saltRounds must be an integer between ${ee} and ${re}`);if(!e.validRoles||e.validRoles.length===0)throw new i("CONFIGURATION_ERROR","validRoles must contain at least one role");if(!e.adapter)throw new i("CONFIGURATION_ERROR","adapter is required")}function h(e){let r=J.get(e);if(r)return r;let t={secret:e.secret,accessExpiresIn:e.accessExpiresIn??"15m",refreshExpiresIn:e.refreshExpiresIn??"7d",algorithm:e.algorithm??"HS256",saltRounds:e.saltRounds??12,validRoles:e.validRoles,validRolesSet:new Set(e.validRoles),adapter:e.adapter,cookieName:e.cookie?.name??"refresh_token",accessCookieName:e.accessCookie?.name??"access_token"};return J.set(e,t),t}var Re=/^(\d+)([smhdw])$/,ge={s:1e3,m:6e4,h:36e5,d:864e5,w:6048e5},te=new Map;function T(e){if(typeof e=="number")return e*1e3;let r=te.get(e);if(r!==void 0)return r;let t=Re.exec(e);if(!t?.[1]||!t?.[2])throw new Error(`Invalid expiresIn: "${e}". Use e.g. "15m", "7d", "1h".`);let s=ge[t[2]];if(s===void 0)throw new Error(`Invalid expiresIn: "${e}". Use e.g. "15m", "7d", "1h".`);let a=parseInt(t[1],10)*s;return te.set(e,a),a}var oe=new Map,ne=new Map;function P(e){let r=oe.get(e);return r||(r={access:`${e}:access`,refresh:`${e}:refresh`},oe.set(e,r)),r}function ie(e,r,t,s){let a=`${t}:${s}`,n=ne.get(a);return n||(n={expiresIn:t,algorithm:s},ne.set(a,n)),K.sign(e,r,n)}function ae(e,r,t){try{let s=K.verify(e,r,{algorithms:[t]});if(typeof s=="string"||s===null)throw new i("TOKEN_INVALID","Token payload is not an object");return s}catch(s){throw s instanceof i?s:s instanceof K.TokenExpiredError?new i("TOKEN_EXPIRED","Token has expired"):new i("TOKEN_INVALID","Token is invalid or malformed")}}function x(e,r){let t=h(r),{access:s}=P(t.secret);return ie(e,s,t.accessExpiresIn,t.algorithm)}function v(e,r){let t=h(r),{refresh:s}=P(t.secret);return ie({sessionId:e},s,t.refreshExpiresIn,t.algorithm)}function b(e,r){let t=h(r),{access:s}=P(t.secret);return ae(e,s,t.algorithm)}function N(e,r){let t=h(r),{refresh:s}=P(t.secret);return ae(e,s,t.algorithm)}function I(e){return h(e).cookieName}function E(e,r){if(!e)return;let t=`${r}=`,s=0;for(;s<e.length;){for(;s<e.length&&e[s]===" ";)s++;let a=e.indexOf(";",s),n=a===-1?e.length:a;if(e.startsWith(t,s))return e.slice(s+t.length,n);s=n+1;}}function O(e,r,t){let s=t.cookie??{},a=h(t),n=T(a.refreshExpiresIn);e.cookie(I(t),r,{httpOnly:s.httpOnly??true,secure:s.secure??false,sameSite:s.sameSite??"strict",path:s.path??"/",maxAge:n});}function L(e,r){let t=r.cookie??{};e.clearCookie(I(r),{path:t.path??"/"});}function M(e){return h(e).accessCookieName}function S(e,r,t){if(!t.accessCookie)return;let s=t.accessCookie,a=h(t),n=T(a.accessExpiresIn);e.cookie(M(t),r,{httpOnly:false,secure:s.secure??false,sameSite:s.sameSite??"strict",path:s.path??"/",maxAge:n});}function X(e,r){if(!r.accessCookie)return;let t=r.accessCookie;e.clearCookie(M(r),{path:t.path??"/"});}function _(e,r){let t=e.headers.authorization;return t?.startsWith("Bearer ")?t.slice(7):E(e.headers.cookie,M(r))}async function Z(e,r){let t=h(r),s=e.roles??[],a=s.filter(u=>!t.validRolesSet.has(u));if(a.length>0)return {success:false,error:new i("INVALID_ROLE",`Invalid roles: ${a.join(", ")}`)};let n=e.identifier.trim();if(await t.adapter.user.findByIdentifier(n))return {success:false,error:new i("USER_ALREADY_EXISTS","User already exists")};let f=await D(e.password,t.saltRounds);return {success:true,user:{id:(await t.adapter.user.create({identifier:n,passwordHash:f,roles:s})).id,identifier:n,roles:s}}}async function ue(e,r){let t=h(r),s=await t.adapter.user.findByIdentifier(e.identifier.trim());if(!s)return {success:false,error:new i("INVALID_CREDENTIALS","Invalid credentials")};if(!await H(e.password,s.passwordHash))return {success:false,error:new i("INVALID_CREDENTIALS","Invalid credentials")};let n=new Date(Date.now()+T(t.refreshExpiresIn)),c=await t.adapter.session.create({userId:s.id,expiresAt:n}),f={id:s.id,identifier:s.identifier,roles:s.roles},m=x({id:s.id,identifier:s.identifier,roles:s.roles,sessionId:c.id},r),o=v(c.id,r);return {success:true,accessToken:m,refreshToken:o,user:f}}async function j(e,r){let t=h(r),s;try{({sessionId:s}=N(e,r));}catch(u){return u instanceof i?{success:false,error:u}:{success:false,error:new i("TOKEN_INVALID","Invalid refresh token")}}let a=await t.adapter.session.findById(s);if(!a)return {success:false,error:new i("UNAUTHORIZED","Session not found or revoked")};if(a.expiresAt.getTime()<Date.now())return await t.adapter.session.delete(s),{success:false,error:new i("TOKEN_EXPIRED","Session has expired")};await t.adapter.session.delete(s);let n=new Date(Date.now()+T(t.refreshExpiresIn)),c=await t.adapter.session.create({userId:a.userId,expiresAt:n}),f={id:a.user.id,identifier:a.user.identifier,roles:a.user.roles},m=x({id:f.id,identifier:f.identifier,roles:f.roles,sessionId:c.id},r),o=v(c.id,r);return {success:true,accessToken:m,refreshToken:o,user:f}}async function ce(e,r){let t;try{({sessionId:t}=N(e,r));}catch{return}await h(r).adapter.session.delete(t);}async function de(e,r){await h(r).adapter.session.deleteAllForUser(e);}async function le(e,r,t){let s=h(t),a=r.filter(m=>!s.validRolesSet.has(m));if(a.length>0)return {success:false,error:new i("INVALID_ROLE",`Invalid roles: ${a.join(", ")}`)};let n=await s.adapter.user.findById(e);if(!n)return {success:false,error:new i("USER_NOT_FOUND","User not found")};let c=new Set(n.roles);for(let m of r)c.add(m);let f=Array.from(c);return await s.adapter.user.updateRoles(e,f),{success:true,user:{id:n.id,identifier:n.identifier,roles:f}}}function k(e){return async(r,t,s)=>{let a=_(r,e);if(!a)return s(new i("UNAUTHORIZED","Missing or malformed Authorization header"));try{let n=b(a,e);if(e.isTokenRevoked&&await e.isTokenRevoked(n.sessionId))return s(new i("UNAUTHORIZED","Token has been revoked"));r.user={id:n.id,identifier:n.identifier,roles:n.roles},s();}catch(n){if(n instanceof i&&n.code==="TOKEN_EXPIRED"){let c=E(r.headers.cookie,I(e));if(!c)return s(new i("UNAUTHORIZED","Token expired. Please login again."));try{let f=await j(c,e);if(!f.success)return s(new i("UNAUTHORIZED","Session expired. Please login again."));O(t,f.refreshToken,e),S(t,f.accessToken,e),t.setHeader("X-New-Access-Token",f.accessToken),r.user=f.user,s();}catch{s(new i("UNAUTHORIZED","Session expired. Please login again."));}}else s(n);}}}function F(...e){let r=`Requires one of roles: ${e.join(", ")}`;return (t,s,a)=>{if(!t.user)return a(new i("UNAUTHORIZED","Not authenticated"));let n=t.user.roles;if(!e.some(c=>n.includes(c)))return a(new i("FORBIDDEN",r));a();}}var Ae=new i("FORBIDDEN","You do not have permission to perform this action");function fe(e){let r=typeof e=="function"?{check:e}:e;return async(t,s,a)=>{if(!t.user)return a(new i("UNAUTHORIZED","Not authenticated"));if(r.roles&&r.roles.length>0){let n=t.user.roles;if(r.roles.some(c=>n.includes(c)))return a()}try{let n=r.check(t);(n instanceof Promise?await n:n)?a():a(Ae);}catch(n){a(n);}}}var pe=8,q=72,$=255;function y(e){return new i("VALIDATION_ERROR",e)}function C(e,r,t,s){e.status(r).json({error:false,statusCode:r,message:t,data:s});}function U(e,r){e.status(r.statusCode).json({error:true,statusCode:r.statusCode,code:r.code,message:r.message,data:null});}function z(e){if(e==null||typeof e!="object"||Array.isArray(e))throw new i("VALIDATION_ERROR","Request body is missing or not a JSON object. Did you apply express.json()?");return e}function Ie(e,r){if(!r.apiKey)return;let t=e.headers["x-api-key"];if(typeof t!="string"||t!==r.apiKey)throw new i("UNAUTHORIZED","Invalid or missing API key")}function B(e){if(e)try{let r=e();r instanceof Promise&&r.catch(()=>{});}catch{}}function he(e){let r=Router(),t=e,s=e.router?.register??(o=>Z(o,t)),a=e.router?.login??(o=>ue(o,t)),n=e.router?.refresh??(o=>j(o,t)),c=e.router?.logout??(o=>o!==void 0?ce(o,t):Promise.resolve()),f=e.router?.logoutAll??(o=>de(o,t)),m=e.router?.assignRoles??((o,u)=>le(o,u,t));return r.post("/register",async(o,u,p)=>{try{Ie(o,e);let d=z(o.body),{identifier:l,password:g,roles:A}=d;if(typeof l!="string"||l.trim().length===0)throw y("identifier is required and must be a non-empty string");if(l.length>$)throw y(`identifier must not exceed ${$} characters`);if(typeof g!="string"||g.length<pe)throw y(`password is required and must be at least ${pe} characters`);if(g.length>q)throw y(`password must not exceed ${q} characters`);if(A!==void 0&&!Array.isArray(A))throw y("roles must be an array of strings when provided");if(Array.isArray(A)&&!A.every(me=>typeof me=="string"))throw y("each role must be a string");let R=Array.isArray(A)?A:void 0,w=R!==void 0?{identifier:l.trim(),password:g,roles:R}:{identifier:l.trim(),password:g},V=await s(w);if(!V.success){U(u,V.error);return}C(u,201,"User registered successfully",{user:V.user});}catch(d){p(d);}}),r.post("/login",async(o,u,p)=>{try{let d=z(o.body),{identifier:l,password:g}=d;if(typeof l!="string"||l.trim().length===0)throw y("identifier is required and must be a non-empty string");if(l.length>$)throw y(`identifier must not exceed ${$} characters`);if(typeof g!="string"||g.length===0)throw y("password is required");if(g.length>q)throw y(`password must not exceed ${q} characters`);let A=l.trim(),R=await a({identifier:A,password:g});if(!R.success){B(()=>e.hooks?.onFailedLogin?.(A,R.error)),U(u,R.error);return}B(()=>e.hooks?.onLogin?.(R.user)),O(u,R.refreshToken,e),S(u,R.accessToken,e),C(u,200,"Login successful",{accessToken:R.accessToken,user:R.user});}catch(d){p(d);}}),r.post("/refresh",async(o,u,p)=>{try{let d=E(o.headers.cookie,I(e));if(!d)throw new i("UNAUTHORIZED","Refresh token cookie is missing");let l=await n(d);if(!l.success){L(u,e),U(u,l.error);return}O(u,l.refreshToken,e),S(u,l.accessToken,e),C(u,200,"Token refreshed",{accessToken:l.accessToken});}catch(d){p(d);}}),r.post("/logout",async(o,u,p)=>{try{let d=E(o.headers.cookie,I(e));await c(d),L(u,e),X(u,e),C(u,200,"Logged out",null);}catch(d){p(d);}}),r.post("/logout-all",k(e),async(o,u,p)=>{try{let d=o.user.id;await f(d),B(()=>e.hooks?.onLogout?.(d)),L(u,e),X(u,e),C(u,200,"All sessions revoked",null);}catch(d){p(d);}}),r.get("/me",k(e),(o,u)=>{C(u,200,"OK",o.user);}),r.post("/users/:userId/roles",k(e),F("admin"),async(o,u,p)=>{try{let d=z(o.body),{roles:l}=d,g=o.params.userId,A=typeof g=="string"?g:void 0;if(!A)throw y("userId is required");if(!Array.isArray(l)||l.length===0)throw y("roles must be a non-empty array of strings");if(!l.every(w=>typeof w=="string"))throw y("each role must be a string");let R=await m(A,l);if(!R.success){U(u,R.error);return}C(u,200,"Roles assigned successfully",{user:R.user});}catch(d){p(d);}}),r.use((o,u,p,d)=>{o instanceof i?U(p,o):p.status(500).json({error:true,statusCode:500,code:"INTERNAL_SERVER_ERROR",message:"Internal server error",data:null});}),r}function G(e){return (r,t,s,a)=>{if(r instanceof i){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,message:"Internal server error",data:null});}}function Ce(e){se(e);let r=h(e);return {protect:()=>k(e),authorize:(...t)=>F(...t),permit:t=>fe(t),hashPassword:t=>D(t,r.saltRounds),verifyPassword:(t,s)=>H(t,s),signAccessToken:t=>x(t,e),signRefreshToken:t=>v(t,e),verifyAccessToken:t=>b(t,e),verifyRefreshToken:t=>N(t,e),getCurrentAccessToken:t=>_(t,e),router:()=>he(e),errorHandler:t=>G(t)}}function we(e){let r=e?.ttl??3e5,t=Math.max(r,5e3),s=(e?.header??"X-Idempotency-Key").toLowerCase(),a=new Set((e?.methods??["POST","PUT","PATCH"]).map(m=>m.toUpperCase())),n=e?.maxSize??1e4,c=new Map,f=setInterval(()=>{let m=Date.now();for(let[o,u]of c)u.expiresAt<=m&&c.delete(o);},t);return typeof f=="object"&&f!==null&&"unref"in f&&f.unref(),(m,o,u)=>{let p=m.headers[s];if(!p||typeof p!="string"||!a.has(m.method))return u();m.requestId=p,o.setHeader("X-Request-Id",p);let d=Date.now(),l=c.get(p);if(l&&l.expiresAt>d)return o.setHeader("X-Idempotent-Replayed","true"),o.status(l.statusCode).json(l.body);let g=o.json.bind(o);o.json=function(R){if(o.statusCode>=200&&o.statusCode<300){if(c.size>=n){let w=c.keys().next().value;w!==void 0&&c.delete(w);}c.set(p,{statusCode:o.statusCode,body:R,expiresAt:Date.now()+r});}return g(R)},u();}}export{Y as AUTH_ERROR_STATUS,i as SentriError,Ce as createAuth,G as createErrorHandler,we as createIdempotencyMiddleware,_ as getCurrentAccessToken,Z as register};
1
+ import xe from'bcrypt';import B from'jsonwebtoken';import {randomUUID,generateKeyPairSync,createPublicKey,createPrivateKey}from'crypto';import {Kysely,sql,PostgresDialect}from'kysely';import {Router}from'express';import {Redis}from'ioredis';import {Pool}from'pg';var be=Object.assign(Object.create(null),{UNAUTHORIZED:401,TOKEN_EXPIRED:401,TOKEN_INVALID:401,INVALID_CREDENTIALS:401,FORBIDDEN:403,USER_NOT_FOUND:404,IDENTIFIER_NOT_FOUND:404,USER_ALREADY_EXISTS:409,IDENTIFIER_ALREADY_EXISTS:409,INVALID_ROLE:400,VALIDATION_ERROR:400,CONFIGURATION_ERROR:500}),o=class extends Error{code;statusCode;constructor(r,s,t){super(s),this.name="SentriError",this.code=r,this.statusCode=t??be[r]??500;}};async function M(e,r=12){return xe.hash(e,r)}async function V(e,r){return xe.compare(e,r)}var Pe=new Map,Ne=new Map,Ar=3600*1e3;function Oe(e){let r=Pe.get(e);if(!r){let s=createPrivateKey(e),t=createPublicKey(s),i=t.export({format:"jwk"}),n=Buffer.from(e).slice(0,8).toString("base64url"),d={...i,use:"sig",kid:n};r={kid:n,publicKey:t,jwk:d},Pe.set(e,r);}return r}function Ue(e){let{jwk:r}=Oe(e);return {keys:[r]}}function Ke(e){return Oe(e).publicKey}async function He(e){let r=Date.now(),s=Ne.get(e);if(s&&r-s.fetchedAt<Ar)return s.publicKey;let t=await fetch(e);if(!t.ok)throw new o("CONFIGURATION_ERROR",`Failed to fetch public key from ${e}: HTTP ${t.status}`);let i=await t.json();if(!i.keys||i.keys.length===0)throw new o("CONFIGURATION_ERROR",`No keys found in JWKS response from ${e}`);let n=i.keys[0],d=createPublicKey({key:n,format:"jwk"});return Ne.set(e,{publicKey:d,fetchedAt:r}),d}var Fe=new WeakMap,je=32,Le=10,qe=31;function Me(e){if(e.mode==="client"){if(!e.keyUri||e.keyUri.trim().length===0)throw new o("CONFIGURATION_ERROR","keyUri must not be empty");return}if(!e.secret||e.secret.trim().length===0)throw new o("CONFIGURATION_ERROR","secret must not be empty");if((e.algorithm??"HS256").startsWith("HS")&&e.secret.length<je)throw new o("CONFIGURATION_ERROR",`secret must be at least ${je} characters for HMAC algorithms`);let t=e.saltRounds??12;if(!Number.isInteger(t)||t<Le||t>qe)throw new o("CONFIGURATION_ERROR",`saltRounds must be an integer between ${Le} and ${qe}`);if(!e.validRoles||e.validRoles.length===0)throw new o("CONFIGURATION_ERROR","validRoles must contain at least one role");if(!e.dialect)throw new o("CONFIGURATION_ERROR","dialect is required in server mode")}function A(e){let r=Fe.get(e);if(r)return r;let s={algorithm:e.algorithm??"HS256",accessExpiresIn:e.accessExpiresIn??"15m",refreshExpiresIn:e.refreshExpiresIn??"7d",saltRounds:e.saltRounds??12,validRoles:e.validRoles,validRolesSet:new Set(e.validRoles),cookieName:e.cookie?.name??"refresh_token",accessCookieName:e.accessCookie?.name??"access_token"};return Fe.set(e,s),s}var vr=/^(\d+)([smhdw])$/,Tr={s:1e3,m:6e4,h:36e5,d:864e5,w:6048e5},$e=new Map;function O(e){if(typeof e=="number")return e*1e3;let r=$e.get(e);if(r!==void 0)return r;let s=vr.exec(e);if(!s?.[1]||!s?.[2])throw new Error(`Invalid expiresIn: "${e}". Use e.g. "15m", "7d", "1h".`);let t=Tr[s[2]];if(t===void 0)throw new Error(`Invalid expiresIn: "${e}". Use e.g. "15m", "7d", "1h".`);let i=parseInt(s[1],10)*t;return $e.set(e,i),i}var Ve=new Map,Be=new Map,Je=new Map;function Xe(e){return e.startsWith("RS")||e.startsWith("PS")}function Ge(e){let r=Ve.get(e);return r||(r={access:`${e}:access`,refresh:`${e}:refresh`},Ve.set(e,r)),r}function Cr(e){let r=Be.get(e);return r||(r=createPrivateKey(e),Be.set(e,r)),r}function ze(e){let r=A(e);if(Xe(r.algorithm)){let i=Cr(e.secret);return {accessKey:i,refreshKey:i}}let{access:s,refresh:t}=Ge(e.secret);return {accessKey:s,refreshKey:t}}function We(e,r){let s=A(e);if(Xe(s.algorithm))return Ke(e.secret);let{access:t,refresh:i}=Ge(e.secret);return r==="access"?t:i}function Ze(e,r,s,t){let i=`${s}:${t}`,n=Je.get(i);return n||(n={expiresIn:s,algorithm:t},Je.set(i,n)),B.sign(e,r,n)}function Ye(e,r,s){try{let t=B.verify(e,r,{algorithms:[s]});if(typeof t=="string"||t===null)throw new o("TOKEN_INVALID","Token payload is not an object");return t}catch(t){throw t instanceof o?t:t instanceof B.TokenExpiredError?new o("TOKEN_EXPIRED","Token has expired"):new o("TOKEN_INVALID","Token is invalid or malformed")}}function J(e,r){let s=A(r),{accessKey:t}=ze(r);return Ze(e,t,s.accessExpiresIn,s.algorithm)}function X(e,r){let s=A(r),{refreshKey:t}=ze(r);return Ze({sessionId:e},t,s.refreshExpiresIn,s.algorithm)}function se(e,r){let s=A(r),t=We(r,"access");return Ye(e,t,s.algorithm)}function G(e,r){let s=A(r),t=We(r,"refresh");return Ye(e,t,s.algorithm)}function Qe(e,r){try{let s=B.verify(e,r,{algorithms:["RS256","RS384","RS512","PS256","PS384","PS512"]});if(typeof s=="string"||s===null)throw new o("TOKEN_INVALID","Token payload is not an object");return s}catch(s){throw s instanceof o?s:s instanceof B.TokenExpiredError?new o("TOKEN_EXPIRED","Token has expired"):new o("TOKEN_INVALID","Token is invalid or malformed")}}function D(e){return A(e).cookieName}function U(e,r){if(!e)return;let s=`${r}=`,t=0;for(;t<e.length;){for(;t<e.length&&e[t]===" ";)t++;let i=e.indexOf(";",t),n=i===-1?e.length:i;if(e.startsWith(s,t))return e.slice(t+s.length,n);t=n+1;}}function z(e,r,s){let t=s.cookie??{},i=A(s),n=O(i.refreshExpiresIn);e.cookie(D(s),r,{httpOnly:t.httpOnly??true,secure:t.secure??false,sameSite:t.sameSite??"strict",path:t.path??"/",maxAge:n});}function ie(e,r){let s=r.cookie??{};e.clearCookie(D(r),{path:s.path??"/"});}function we(e){return A(e).accessCookieName}function W(e,r,s){if(!s.accessCookie)return;let t=s.accessCookie,i=A(s),n=O(i.accessExpiresIn);e.cookie(we(s),r,{httpOnly:false,secure:t.secure??false,sameSite:t.sameSite??"strict",path:t.path??"/",maxAge:n});}function Ie(e,r){if(!r.accessCookie)return;let s=r.accessCookie;e.clearCookie(we(r),{path:s.path??"/"});}function Z(e,r){let s=e.headers.authorization;return s?.startsWith("Bearer ")?s.slice(7):U(e.headers.cookie,we(r))}var er=new Map;function T(e){let r=er.get(e);return r||(r=new Kysely({dialect:e}),er.set(e,r)),r}function Ae(e){try{return JSON.parse(e)}catch{return []}}function Sr(e){return JSON.stringify(e)}function tr(e){return {id:e.id,userId:e.user_id,type:e.type,value:e.value,isPrimary:e.is_primary===1,createdAt:new Date(e.created_at)}}async function Y(e,r){let s=await e.selectFrom("sentri_identifiers as i_login").innerJoin("sentri_users as u","u.id","i_login.user_id").innerJoin("sentri_identifiers as i_primary",t=>t.onRef("i_primary.user_id","=","u.id").on("i_primary.is_primary","=",1)).select(["u.id","u.password_hash","u.roles","i_primary.value as primary_value","i_primary.type as primary_type"]).where("i_login.value","=",r).executeTakeFirst();return s?{id:s.id,identifier:s.primary_value,identifierType:s.primary_type,passwordHash:s.password_hash,roles:Ae(s.roles)}:null}async function ne(e,r){let s=await e.selectFrom("sentri_users as u").innerJoin("sentri_identifiers as i",t=>t.onRef("i.user_id","=","u.id").on("i.is_primary","=",1)).select(["u.id","u.password_hash","u.roles","i.value as primary_value","i.type as primary_type"]).where("u.id","=",r).executeTakeFirst();return s?{id:s.id,identifier:s.primary_value,identifierType:s.primary_type,passwordHash:s.password_hash,roles:Ae(s.roles)}:null}async function sr(e,r,s){let t=s.map(i=>({id:randomUUID(),user_id:r,type:i.type,value:i.value,is_primary:i.isPrimary?1:0}));return await e.insertInto("sentri_identifiers").values(t).execute(),t.map(i=>({id:i.id,userId:i.user_id,type:i.type,value:i.value,isPrimary:i.is_primary===1,createdAt:new Date}))}async function K(e,r){return (await e.selectFrom("sentri_identifiers").selectAll().where("user_id","=",r).orderBy("created_at","asc").execute()).map(tr)}async function oe(e,r,s){let t=await e.selectFrom("sentri_identifiers").selectAll().where("id","=",r).where("user_id","=",s).executeTakeFirst();return t?tr(t):null}async function ir(e,r){let s=await e.selectFrom("sentri_identifiers").select(t=>t.fn.countAll().as("count")).where("user_id","=",r).executeTakeFirst();return Number(s?.count??0)}async function nr(e,r,s,t){await e.updateTable("sentri_identifiers").set({type:t.type,value:t.value}).where("id","=",r).where("user_id","=",s).execute();}async function or(e,r,s){await e.deleteFrom("sentri_identifiers").where("user_id","=",r).where("id","in",s).execute();}async function ar(e,r,s){await e.transaction().execute(async t=>{await t.updateTable("sentri_identifiers").set({is_primary:0}).where("user_id","=",r).execute(),await t.updateTable("sentri_identifiers").set({is_primary:1}).where("id","=",s).where("user_id","=",r).execute();});}async function ur(e,r,s){await e.updateTable("sentri_users").set({password_hash:s}).where("id","=",r).execute();}async function dr(e,r,s){await e.updateTable("sentri_users").set({roles:Sr(s)}).where("id","=",r).execute();}async function ve(e,r){let s=randomUUID();return await e.insertInto("sentri_sessions").values({id:s,user_id:r.userId,expires_at:r.expiresAt}).execute(),{id:s}}async function cr(e,r){let s=await e.selectFrom("sentri_sessions as s").innerJoin("sentri_users as u","u.id","s.user_id").innerJoin("sentri_identifiers as i",t=>t.onRef("i.user_id","=","u.id").on("i.is_primary","=",1)).select(["s.id as session_id","s.user_id","s.expires_at","s.created_at as session_created_at","u.id as user_id_col","u.password_hash","u.roles","i.value as primary_identifier","i.type as primary_identifier_type"]).where("s.id","=",r).executeTakeFirst();return s?{id:s.session_id,userId:s.user_id,expiresAt:new Date(s.expires_at),createdAt:new Date(s.session_created_at),user:{id:s.user_id_col,identifier:s.primary_identifier,identifierType:s.primary_identifier_type,passwordHash:s.password_hash,roles:Ae(s.roles)}}:null}async function ae(e,r){await e.deleteFrom("sentri_sessions").where("id","=",r).execute();}async function Te(e,r){await e.deleteFrom("sentri_sessions").where("user_id","=",r).execute();}async function Q(e,r){let s=A(r),t=T(r.dialect),i=e.roles??[],n=i.filter(R=>!s.validRolesSet.has(R));if(n.length>0)return {success:false,error:new o("INVALID_ROLE",`Invalid roles: ${n.join(", ")}`)};if(!e.identifiers||e.identifiers.length===0)return {success:false,error:new o("VALIDATION_ERROR","At least one identifier is required")};let d=e.identifiers.map(R=>({type:R.type.trim(),value:R.value.trim()}));if(new Set(d.map(R=>R.value)).size!==d.length)return {success:false,error:new o("VALIDATION_ERROR","Duplicate identifier values in request")};for(let R of d)if(await Y(t,R.value))return {success:false,error:new o("IDENTIFIER_ALREADY_EXISTS",`Identifier already taken: ${R.value}`)};let l=await M(e.password,s.saltRounds),{userId:g,identifierRows:v}=await t.transaction().execute(async R=>{let x=randomUUID();await R.insertInto("sentri_users").values({id:x,password_hash:l,roles:JSON.stringify(i)}).execute();let E=d.map((u,c)=>({id:randomUUID(),user_id:x,type:u.type,value:u.value,is_primary:c===0?1:0}));return await R.insertInto("sentri_identifiers").values(E).execute(),{userId:x,identifierRows:E}}),S=v.map(R=>({id:R.id,type:R.type,value:R.value,isPrimary:R.is_primary===1})),k=S[0];return {success:true,user:{id:g,identifier:k.value,identifierType:k.type,roles:i,identifiers:S}}}async function ue(e,r){let s=A(r),t=T(r.dialect),i=await Y(t,e.identifier.trim());if(!i)return {success:false,error:new o("INVALID_CREDENTIALS","Invalid credentials")};if(!await V(e.password,i.passwordHash))return {success:false,error:new o("INVALID_CREDENTIALS","Invalid credentials")};let d=new Date(Date.now()+O(s.refreshExpiresIn)),a=await ve(t,{userId:i.id,expiresAt:d}),l={id:i.id,identifier:i.identifier,identifierType:i.identifierType,roles:i.roles},g=J({id:i.id,identifier:i.identifier,identifierType:i.identifierType,roles:i.roles,sessionId:a.id},r),v=X(a.id,r);return {success:true,accessToken:g,refreshToken:v,user:l}}async function H(e,r){let s=A(r),t=T(r.dialect),i;try{({sessionId:i}=G(e,r));}catch(S){return S instanceof o?{success:false,error:S}:{success:false,error:new o("TOKEN_INVALID","Invalid refresh token")}}let n=await cr(t,i);if(!n)return {success:false,error:new o("UNAUTHORIZED","Session not found or revoked")};if(n.expiresAt.getTime()<Date.now())return await ae(t,i),{success:false,error:new o("TOKEN_EXPIRED","Session has expired")};await ae(t,i);let d=new Date(Date.now()+O(s.refreshExpiresIn)),a=await ve(t,{userId:n.userId,expiresAt:d}),l={id:n.user.id,identifier:n.user.identifier,identifierType:n.user.identifierType,roles:n.user.roles},g=J({...l,sessionId:a.id},r),v=X(a.id,r);return {success:true,accessToken:g,refreshToken:v,user:l}}async function de(e,r){let s=T(r.dialect),t;try{({sessionId:t}=G(e,r));}catch{return}await ae(s,t);}async function ce(e,r){let s=T(r.dialect);await Te(s,e);}async function fr(e,r){let s=T(r.dialect),t=await ne(s,e);if(!t)return {success:false,error:new o("USER_NOT_FOUND","User not found")};let i=await K(s,e);return {success:true,user:{id:t.id,identifier:t.identifier,identifierType:t.identifierType,roles:t.roles,identifiers:i.map(n=>({id:n.id,type:n.type,value:n.value,isPrimary:n.isPrimary}))}}}async function le(e,r,s,t){let i=A(t),n=T(t.dialect),d=await ne(n,e);if(!d)return {success:false,error:new o("USER_NOT_FOUND","User not found")};if(!await V(r,d.passwordHash))return {success:false,error:new o("INVALID_CREDENTIALS","Invalid credentials")};let l=await M(s,i.saltRounds);return await ur(n,e,l),await Te(n,e),{success:true}}async function fe(e,r,s){let t=A(s),i=T(s.dialect),n=r.filter(g=>!t.validRolesSet.has(g));if(n.length>0)return {success:false,error:new o("INVALID_ROLE",`Invalid roles: ${n.join(", ")}`)};let d=await ne(i,e);if(!d)return {success:false,error:new o("USER_NOT_FOUND","User not found")};let a=new Set(d.roles);for(let g of r)a.add(g);let l=Array.from(a);return await dr(i,e,l),{success:true,user:{id:d.id,identifier:d.identifier,identifierType:d.identifierType,roles:l}}}async function pe(e,r,s){let t=T(s.dialect);if(r.length===0)return {success:false,error:new o("VALIDATION_ERROR","At least one identifier is required")};let i=r.map(l=>({type:l.type.trim(),value:l.value.trim()}));if(new Set(i.map(l=>l.value)).size!==i.length)return {success:false,error:new o("VALIDATION_ERROR","Duplicate identifier values in request")};for(let l of i)if(await Y(t,l.value))return {success:false,error:new o("IDENTIFIER_ALREADY_EXISTS",`Identifier already taken: ${l.value}`)};await sr(t,e,i.map(l=>({...l,isPrimary:false})));return {success:true,identifiers:(await K(t,e)).map(l=>({id:l.id,type:l.type,value:l.value,isPrimary:l.isPrimary}))}}async function me(e,r,s){let t=T(s.dialect);if(r.length===0)return {success:false,error:new o("VALIDATION_ERROR","At least one update is required")};let i=r.map(a=>({id:a.id,type:a.type.trim(),value:a.value.trim()}));if(new Set(i.map(a=>a.value)).size!==i.length)return {success:false,error:new o("VALIDATION_ERROR","Duplicate identifier values in request")};for(let a of i){let l=await oe(t,a.id,e);if(!l)return {success:false,error:new o("IDENTIFIER_NOT_FOUND",`Identifier not found: ${a.id}`)};if(l.value!==a.value&&await Y(t,a.value))return {success:false,error:new o("IDENTIFIER_ALREADY_EXISTS",`Identifier already taken: ${a.value}`)}}for(let a of i)await nr(t,a.id,e,{type:a.type,value:a.value});return {success:true,identifiers:(await K(t,e)).map(a=>({id:a.id,type:a.type,value:a.value,isPrimary:a.isPrimary}))}}async function ye(e,r,s){let t=T(s.dialect);if(r.length===0)return {success:false,error:new o("VALIDATION_ERROR","At least one ID is required")};for(let d of r)if(!await oe(t,d,e))return {success:false,error:new o("IDENTIFIER_NOT_FOUND",`Identifier not found: ${d}`)};return await ir(t,e)-r.length<1?{success:false,error:new o("VALIDATION_ERROR","Cannot delete all identifiers \u2014 at least one must remain")}:(await or(t,e,r),{success:true,identifiers:(await K(t,e)).map(d=>({id:d.id,type:d.type,value:d.value,isPrimary:d.isPrimary}))})}async function ge(e,r,s){let t=T(s.dialect);return await oe(t,r,e)?(await ar(t,e,r),{success:true,identifiers:(await K(t,e)).map(d=>({id:d.id,type:d.type,value:d.value,isPrimary:d.isPrimary}))}):{success:false,error:new o("IDENTIFIER_NOT_FOUND",`Identifier not found: ${r}`)}}function C(e){return e.mode==="client"?Er(e.keyUri):br(e)}function Er(e){return async(r,s,t)=>{let i=r.headers.authorization,n=i?.startsWith("Bearer ")?i.slice(7):void 0;if(!n)return t(new o("UNAUTHORIZED","Missing or malformed Authorization header"));try{let d=await He(e),a=Qe(n,d);r.user={id:a.id,identifier:a.identifier,identifierType:a.identifierType,roles:a.roles},t();}catch(d){t(d);}}}function br(e){return async(r,s,t)=>{let i=Z(r,e);if(!i)return t(new o("UNAUTHORIZED","Missing or malformed Authorization header"));try{let n=se(i,e);if(e.isTokenRevoked&&await e.isTokenRevoked(n.sessionId))return t(new o("UNAUTHORIZED","Token has been revoked"));r.user={id:n.id,identifier:n.identifier,identifierType:n.identifierType,roles:n.roles},t();}catch(n){if(n instanceof o&&n.code==="TOKEN_EXPIRED"){let d=U(r.headers.cookie,D(e));if(!d)return t(new o("UNAUTHORIZED","Token expired. Please login again."));try{let a=await H(d,e);if(!a.success)return t(new o("UNAUTHORIZED","Session expired. Please login again."));z(s,a.refreshToken,e),W(s,a.accessToken,e),s.setHeader("X-New-Access-Token",a.accessToken),r.user=a.user,t();}catch{t(new o("UNAUTHORIZED","Session expired. Please login again."));}}else t(n);}}}function ee(...e){let r=`Requires one of roles: ${e.join(", ")}`;return (s,t,i)=>{if(!s.user)return i(new o("UNAUTHORIZED","Not authenticated"));let n=s.user.roles;if(!e.some(d=>n.includes(d)))return i(new o("FORBIDDEN",r));i();}}var xr=new o("FORBIDDEN","You do not have permission to perform this action");function re(e){let r=typeof e=="function"?{check:e}:e;return async(s,t,i)=>{if(!s.user)return i(new o("UNAUTHORIZED","Not authenticated"));if(r.roles&&r.roles.length>0){let n=s.user.roles;if(r.roles.some(d=>n.includes(d)))return i()}try{let n=r.check(s);(n instanceof Promise?await n:n)?i():i(xr);}catch(n){i(n);}}}function F(e){return (r,s,t,i)=>{if(r instanceof o){t.status(r.statusCode).json({error:true,statusCode:r.statusCode,code:r.code,message:r.message,data:null});return}e?.onUnhandled?.(r),t.status(500).json({error:true,statusCode:500,code:"INTERNAL_SERVER_ERROR",message:"Internal server error",data:null});}}var he=8,j=72,Re=255,pr=100,L=50;function y(e){return new o("VALIDATION_ERROR",e)}function _(e,r,s,t){e.status(r).json({error:false,statusCode:r,message:s,data:t});}function P(e,r){e.status(r.statusCode).json({error:true,statusCode:r.statusCode,code:r.code,message:r.message,data:null});}function N(e){if(e==null||typeof e!="object"||Array.isArray(e))throw new o("VALIDATION_ERROR","Request body is missing or not a JSON object. Did you apply express.json()?");return e}function Nr(e,r){if(!r.apiKey)return;let s=e.headers["x-api-key"];if(typeof s!="string"||s!==r.apiKey)throw new o("UNAUTHORIZED","Invalid or missing API key")}function ke(e){if(e)try{let r=e();r instanceof Promise&&r.catch(()=>{});}catch{}}function Dr(e){return e.startsWith("RS")||e.startsWith("PS")}function Ce(e,r){if(typeof e!="object"||e===null||Array.isArray(e))throw y(`identifiers[${r}] must be an object`);let s=e;if(typeof s.type!="string"||s.type.trim().length===0)throw y(`identifiers[${r}].type is required and must be a non-empty string`);if(s.type.length>pr)throw y(`identifiers[${r}].type must not exceed ${pr} characters`);if(typeof s.value!="string"||s.value.trim().length===0)throw y(`identifiers[${r}].value is required and must be a non-empty string`);if(s.value.length>Re)throw y(`identifiers[${r}].value must not exceed ${Re} characters`);return {type:s.type,value:s.value}}function mr(e){let r=Router(),s=e,t=A(s),i=e.router?.register??(u=>Q(u,s)),n=e.router?.login??(u=>ue(u,s)),d=e.router?.refresh??(u=>H(u,s)),a=e.router?.logout??(u=>u!==void 0?de(u,s):Promise.resolve()),l=e.router?.logoutAll??(u=>ce(u,s)),g=e.router?.assignRoles??((u,c)=>fe(u,c,s)),v=e.router?.changePassword??((u,c,w)=>le(u,c,w,s)),S=e.router?.bulkCreateIdentifiers??((u,c)=>pe(u,c,s)),k=e.router?.bulkUpdateIdentifiers??((u,c)=>me(u,c,s)),R=e.router?.bulkDeleteIdentifiers??((u,c)=>ye(u,c,s)),x=e.router?.changePrimaryIdentifier??((u,c)=>ge(u,c,s));Dr(t.algorithm)&&r.get("/keys",(u,c)=>{c.setHeader("Cache-Control","public, max-age=3600"),c.json(Ue(e.secret));}),r.post("/register",async(u,c,w)=>{try{Nr(u,e);let f=N(u.body),{identifiers:p,password:m,roles:h}=f;if(!Array.isArray(p)||p.length===0)throw y("identifiers is required and must be a non-empty array");if(p.length>L)throw y(`identifiers must not exceed ${L} entries`);let I=p.map(($,wr)=>Ce($,wr));if(typeof m!="string"||m.length<he)throw y(`password is required and must be at least ${he} characters`);if(m.length>j)throw y(`password must not exceed ${j} characters`);if(h!==void 0&&!Array.isArray(h))throw y("roles must be an array of strings when provided");if(Array.isArray(h)&&!h.every($=>typeof $=="string"))throw y("each role must be a string");let b=Array.isArray(h)?h:void 0,q=await i(b!==void 0?{identifiers:I,password:m,roles:b}:{identifiers:I,password:m});if(!q.success){P(c,q.error);return}_(c,201,"User registered successfully",{user:q.user});}catch(f){w(f);}}),r.post("/login",async(u,c,w)=>{try{let f=N(u.body),{identifier:p,password:m}=f;if(typeof p!="string"||p.trim().length===0)throw y("identifier is required and must be a non-empty string");if(p.length>Re)throw y(`identifier must not exceed ${Re} characters`);if(typeof m!="string"||m.length===0)throw y("password is required");if(m.length>j)throw y(`password must not exceed ${j} characters`);let h=p.trim(),I=await n({identifier:h,password:m});if(!I.success){ke(()=>e.hooks?.onFailedLogin?.(h,I.error)),P(c,I.error);return}ke(()=>e.hooks?.onLogin?.(I.user)),z(c,I.refreshToken,e),W(c,I.accessToken,e),_(c,200,"Login successful",{accessToken:I.accessToken,user:I.user});}catch(f){w(f);}}),r.post("/refresh",async(u,c,w)=>{try{let f=U(u.headers.cookie,D(e));if(!f)throw new o("UNAUTHORIZED","Refresh token cookie is missing");let p=await d(f);if(!p.success){ie(c,e),P(c,p.error);return}z(c,p.refreshToken,e),W(c,p.accessToken,e),_(c,200,"Token refreshed",{accessToken:p.accessToken});}catch(f){w(f);}}),r.post("/logout",async(u,c,w)=>{try{let f=U(u.headers.cookie,D(e));await a(f),ie(c,e),Ie(c,e),_(c,200,"Logged out",null);}catch(f){w(f);}}),r.post("/logout-all",C(e),async(u,c,w)=>{try{let f=u.user.id;await l(f),ke(()=>e.hooks?.onLogout?.(f)),ie(c,e),Ie(c,e),_(c,200,"All sessions revoked",null);}catch(f){w(f);}}),r.get("/me",C(e),(u,c)=>{_(c,200,"OK",u.user);});let E=re(u=>!!u.user);return r.post("/me/identifiers",C(e),E,async(u,c,w)=>{try{let f=N(u.body),{identifiers:p}=f;if(!Array.isArray(p)||p.length===0)throw y("identifiers is required and must be a non-empty array");if(p.length>L)throw y(`identifiers must not exceed ${L} entries`);let m=p.map((I,b)=>Ce(I,b)),h=await S(u.user.id,m);if(!h.success){P(c,h.error);return}_(c,201,"Identifiers added successfully",{identifiers:h.identifiers});}catch(f){w(f);}}),r.put("/me/identifiers",C(e),E,async(u,c,w)=>{try{let f=N(u.body),{identifiers:p}=f;if(!Array.isArray(p)||p.length===0)throw y("identifiers is required and must be a non-empty array");if(p.length>L)throw y(`identifiers must not exceed ${L} entries`);let m=p.map((I,b)=>{if(typeof I!="object"||I===null||Array.isArray(I))throw y(`identifiers[${b}] must be an object`);let te=I;if(typeof te.id!="string"||te.id.trim().length===0)throw y(`identifiers[${b}].id is required and must be a non-empty string`);let{type:q,value:$}=Ce(I,b);return {id:te.id,type:q,value:$}}),h=await k(u.user.id,m);if(!h.success){P(c,h.error);return}_(c,200,"Identifiers updated successfully",{identifiers:h.identifiers});}catch(f){w(f);}}),r.delete("/me/identifiers",C(e),E,async(u,c,w)=>{try{let f=N(u.body),{ids:p}=f;if(!Array.isArray(p)||p.length===0)throw y("ids is required and must be a non-empty array of strings");if(!p.every(h=>typeof h=="string"))throw y("each id must be a string");let m=await R(u.user.id,p);if(!m.success){P(c,m.error);return}_(c,200,"Identifiers deleted successfully",{identifiers:m.identifiers});}catch(f){w(f);}}),r.patch("/me/identifiers/primary",C(e),E,async(u,c,w)=>{try{let f=N(u.body),{id:p}=f;if(typeof p!="string"||p.trim().length===0)throw y("id is required and must be a non-empty string");let m=await x(u.user.id,p.trim());if(!m.success){P(c,m.error);return}_(c,200,"Primary identifier updated successfully",{identifiers:m.identifiers});}catch(f){w(f);}}),r.patch("/me/password",C(e),E,async(u,c,w)=>{try{let f=N(u.body),{currentPassword:p,newPassword:m}=f;if(typeof p!="string"||p.length===0)throw y("currentPassword is required");if(typeof m!="string"||m.length<he)throw y(`newPassword must be at least ${he} characters`);if(m.length>j)throw y(`newPassword must not exceed ${j} characters`);if(p===m)throw y("newPassword must be different from currentPassword");let h=await v(u.user.id,p,m);if(!h.success){P(c,h.error);return}_(c,200,"Password updated successfully. All sessions have been revoked.",null);}catch(f){w(f);}}),r.post("/users/:userId/roles",C(e),ee("admin"),async(u,c,w)=>{try{let f=N(u.body),{roles:p}=f,m=u.params.userId,h=typeof m=="string"?m:void 0;if(!h)throw y("userId is required");if(!Array.isArray(p)||p.length===0)throw y("roles must be a non-empty array of strings");if(!p.every(b=>typeof b=="string"))throw y("each role must be a string");let I=await g(h,p);if(!I.success){P(c,I.error);return}_(c,200,"Roles assigned successfully",{user:I.user});}catch(f){w(f);}}),r.use(F()),r}var yr=new Map;function gr(e){let r=yr.get(e);return r||(r=new Redis(e,{lazyConnect:false,enableOfflineQueue:false}),yr.set(e,r)),r}function _e(e){let r=e?.ttl??3e5,s=(e?.header??"X-Idempotency-Key").toLowerCase(),t=new Set((e?.methods??["POST","PUT","PATCH"]).map(n=>n.toUpperCase())),i=e?.redisUrl;return i?Ur(i,r,s,t):Kr(r,s,t,e?.maxSize??1e4)}function Ur(e,r,s,t){let i=gr(e),n="sentri:idempotency:";return async(d,a,l)=>{let g=d.headers[s];if(!g||typeof g!="string"||!t.has(d.method))return l();d.requestId=g,a.setHeader("X-Request-Id",g);let v=await i.get(`${n}${g}`);if(v){let k=JSON.parse(v);return a.setHeader("X-Idempotent-Replayed","true"),a.status(k.statusCode).json(k.body)}let S=a.json.bind(a);a.json=function(R){if(a.statusCode>=200&&a.statusCode<300){let x={statusCode:a.statusCode,body:R,expiresAt:Date.now()+r};i.set(`${n}${g}`,JSON.stringify(x),"PX",r).catch(()=>{});}return S(R)},l();}}function Kr(e,r,s,t){let i=Math.max(e,5e3),n=new Map,d=setInterval(()=>{let a=Date.now();for(let[l,g]of n)g.expiresAt<=a&&n.delete(l);},i);return typeof d=="object"&&d!==null&&"unref"in d&&d.unref(),(a,l,g)=>{let v=a.headers[r];if(!v||typeof v!="string"||!s.has(a.method))return g();a.requestId=v,l.setHeader("X-Request-Id",v);let S=Date.now(),k=n.get(v);if(k&&k.expiresAt>S)return l.setHeader("X-Idempotent-Replayed","true"),l.status(k.statusCode).json(k.body);let R=l.json.bind(l);l.json=function(E){if(l.statusCode>=200&&l.statusCode<300){if(n.size>=t){let u=n.keys().next().value;u!==void 0&&n.delete(u);}n.set(v,{statusCode:l.statusCode,body:E,expiresAt:Date.now()+e});}return R(E)},g();}}async function hr(e){await e.schema.createTable("sentri_users").ifNotExists().addColumn("id","varchar(36)",r=>r.primaryKey().notNull()).addColumn("password_hash","varchar(255)",r=>r.notNull()).addColumn("roles","text",r=>r.notNull().defaultTo("[]")).addColumn("created_at","timestamp",r=>r.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)).execute(),await e.schema.createTable("sentri_sessions").ifNotExists().addColumn("id","varchar(36)",r=>r.primaryKey().notNull()).addColumn("user_id","varchar(36)",r=>r.notNull().references("sentri_users.id").onDelete("cascade")).addColumn("expires_at","timestamp",r=>r.notNull()).addColumn("created_at","timestamp",r=>r.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)).execute(),await e.schema.createTable("sentri_identifiers").ifNotExists().addColumn("id","varchar(36)",r=>r.primaryKey().notNull()).addColumn("user_id","varchar(36)",r=>r.notNull().references("sentri_users.id").onDelete("cascade")).addColumn("type","varchar(100)",r=>r.notNull()).addColumn("value","varchar(255)",r=>r.notNull().unique()).addColumn("is_primary","integer",r=>r.notNull().defaultTo(0)).addColumn("created_at","timestamp",r=>r.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)).execute();}function Ee(e){if(Me(e),e.mode==="client")return {protect:()=>C(e),authorize:(...t)=>ee(...t),permit:t=>re(t),errorHandler:t=>F(t)};let r=e,s=A(r);return {protect:()=>C(r),authorize:(...t)=>ee(...t),permit:t=>re(t),hashPassword:t=>M(t,s.saltRounds),verifyPassword:(t,i)=>V(t,i),signAccessToken:t=>J(t,r),signRefreshToken:t=>X(t,r),verifyAccessToken:t=>se(t,r),verifyRefreshToken:t=>G(t,r),getCurrentAccessToken:t=>Z(t,r),router:()=>mr(r),migrate:()=>hr(T(r.dialect)),errorHandler:t=>F(t),idempotencyMiddleware:t=>_e({...t,...r.redisUrl!==void 0&&{redisUrl:r.redisUrl}}),register:t=>Q(t,r),login:t=>ue(t,r),refresh:t=>H(t,r),logout:t=>de(t,r),logoutAll:t=>ce(t,r),getUser:t=>fr(t,r),changePassword:(t,i,n)=>le(t,i,n,r),assignRoles:(t,i)=>fe(t,i,r),bulkCreateIdentifiers:(t,i)=>pe(t,i,r),bulkUpdateIdentifiers:(t,i)=>me(t,i,r),bulkDeleteIdentifiers:(t,i)=>ye(t,i,r),changePrimaryIdentifier:(t,i)=>ge(t,i,r)}}function Rr(e){return new PostgresDialect({pool:new Pool(e)})}function Lr(e){let{privateKey:r}=generateKeyPairSync("rsa",{modulusLength:2048,privateKeyEncoding:{type:"pkcs8",format:"pem"},publicKeyEncoding:{type:"spki",format:"pem"}}),s={mode:"server",dialect:Rr(e.db),secret:r,algorithm:"RS256",validRoles:e.validRoles,...e.accessExpiresIn!==void 0&&{accessExpiresIn:e.accessExpiresIn},...e.refreshExpiresIn!==void 0&&{refreshExpiresIn:e.refreshExpiresIn},...e.saltRounds!==void 0&&{saltRounds:e.saltRounds},...e.apiKey!==void 0&&{apiKey:e.apiKey},...e.cookie!==void 0&&{cookie:e.cookie},...e.accessCookie!==void 0&&{accessCookie:e.accessCookie},...e.hooks!==void 0&&{hooks:e.hooks},...e.router!==void 0&&{router:e.router},...e.isTokenRevoked!==void 0&&{isTokenRevoked:e.isTokenRevoked},...e.redisUrl!==void 0&&{redisUrl:e.redisUrl}};return Ee(s)}export{be as SENTRI_ERROR_STATUS,o as SentriError,Ee as createAuth,Lr as createAuthServer,F as createErrorHandler,_e as createIdempotencyMiddleware,Z as getCurrentAccessToken,Q as register};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentri",
3
- "version": "2.0.0",
3
+ "version": "4.0.0",
4
4
  "description": "Personal auth/authorization library for Express + Postgres",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -16,8 +16,7 @@
16
16
  },
17
17
  "files": [
18
18
  "dist",
19
- "bin",
20
- "templates"
19
+ "bin"
21
20
  ],
22
21
  "scripts": {
23
22
  "build": "tsup",
@@ -41,6 +40,7 @@
41
40
  "@types/express": "^5.0.6",
42
41
  "@types/jsonwebtoken": "^9.0.10",
43
42
  "@types/node": "^22.20.0",
43
+ "@types/pg": "^8.11.10",
44
44
  "@types/supertest": "^7.2.0",
45
45
  "@vitest/coverage-v8": "^4.1.9",
46
46
  "express": "^5.2.1",
@@ -52,17 +52,12 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "bcrypt": "^6.0.0",
55
- "jsonwebtoken": "^9.0.3"
55
+ "ioredis": "^5.11.1",
56
+ "jsonwebtoken": "^9.0.3",
57
+ "kysely": "^0.27.4",
58
+ "pg": "^8.13.1"
56
59
  },
57
60
  "peerDependencies": {
58
61
  "express": ">=4.0.0"
59
- },
60
- "peerDependenciesMeta": {
61
- "@prisma/client": {
62
- "optional": true
63
- },
64
- "drizzle-orm": {
65
- "optional": true
66
- }
67
62
  }
68
63
  }
@@ -1,154 +0,0 @@
1
- // sentri — Drizzle adapter template
2
- // Generated by: npx sentri generate drizzle
3
- //
4
- // Adjust the import paths to match your project structure.
5
- // This template assumes tables: users, roles, userRoles, sessions.
6
- // Column names follow the sentri Drizzle schema convention.
7
- //
8
- // Replace NodePgDatabase with the type that matches your Drizzle driver:
9
- // import { type NodePgDatabase } from 'drizzle-orm/node-postgres'; (default)
10
- // import { type LibSQLDatabase } from 'drizzle-orm/libsql';
11
- // import { type MySql2Database } from 'drizzle-orm/mysql2';
12
-
13
- import { eq } from 'drizzle-orm';
14
- import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
15
- import { AuthError, type AuthAdapter } from 'sentri';
16
- import { users, roles, userRoles, sessions } from './schema.js';
17
-
18
- export function createAdapter(db: NodePgDatabase): AuthAdapter {
19
- if (!db) {
20
- throw new AuthError(
21
- 'CONFIGURATION_ERROR',
22
- 'createAdapter requires a Drizzle db instance. Did you forget to pass it?\n' +
23
- 'Example: createAdapter(db)',
24
- );
25
- }
26
- return {
27
- user: {
28
- async findByIdentifier(identifier) {
29
- const rows = await db
30
- .select({
31
- id: users.id,
32
- identifier: users.identifier,
33
- passwordHash: users.passwordHash,
34
- roleName: roles.name,
35
- })
36
- .from(users)
37
- .leftJoin(userRoles, eq(users.id, userRoles.userId))
38
- .leftJoin(roles, eq(userRoles.roleId, roles.id))
39
- .where(eq(users.identifier, identifier));
40
-
41
- if (rows.length === 0) return null;
42
- return {
43
- id: rows[0]!.id,
44
- identifier: rows[0]!.identifier,
45
- passwordHash: rows[0]!.passwordHash,
46
- roles: rows.flatMap((row) => (row.roleName ? [row.roleName] : [])),
47
- };
48
- },
49
-
50
- async findById(id) {
51
- const rows = await db
52
- .select({
53
- id: users.id,
54
- identifier: users.identifier,
55
- passwordHash: users.passwordHash,
56
- roleName: roles.name,
57
- })
58
- .from(users)
59
- .leftJoin(userRoles, eq(users.id, userRoles.userId))
60
- .leftJoin(roles, eq(userRoles.roleId, roles.id))
61
- .where(eq(users.id, id));
62
-
63
- if (rows.length === 0) return null;
64
- return {
65
- id: rows[0]!.id,
66
- identifier: rows[0]!.identifier,
67
- passwordHash: rows[0]!.passwordHash,
68
- roles: rows.flatMap((row) => (row.roleName ? [row.roleName] : [])),
69
- };
70
- },
71
-
72
- async create({ identifier, passwordHash, roles: roleNames }) {
73
- const [user] = await db
74
- .insert(users)
75
- .values({ identifier, passwordHash })
76
- .returning({ id: users.id });
77
-
78
- if (roleNames.length > 0) {
79
- await Promise.all(
80
- roleNames.map(async (name) => {
81
- const existing = await db.select().from(roles).where(eq(roles.name, name));
82
- const role = existing[0] ?? (await db.insert(roles).values({ name }).returning())[0];
83
- await db.insert(userRoles).values({ userId: user!.id, roleId: role!.id });
84
- }),
85
- );
86
- }
87
- return { id: user!.id };
88
- },
89
-
90
- async updateRoles(userId, roleNames) {
91
- await db.delete(userRoles).where(eq(userRoles.userId, userId));
92
- await Promise.all(
93
- roleNames.map(async (name) => {
94
- const existing = await db.select().from(roles).where(eq(roles.name, name));
95
- const role = existing[0] ?? (await db.insert(roles).values({ name }).returning())[0];
96
- await db.insert(userRoles).values({ userId, roleId: role!.id });
97
- }),
98
- );
99
- },
100
- },
101
-
102
- session: {
103
- async create({ userId, expiresAt }) {
104
- const [session] = await db
105
- .insert(sessions)
106
- .values({ userId, expiresAt })
107
- .returning({ id: sessions.id });
108
- return { id: session!.id };
109
- },
110
-
111
- async findById(sessionId) {
112
- const rows = await db
113
- .select({
114
- sessionId: sessions.id,
115
- sessionUserId: sessions.userId,
116
- sessionExpiresAt: sessions.expiresAt,
117
- sessionCreatedAt: sessions.createdAt,
118
- userId: users.id,
119
- identifier: users.identifier,
120
- passwordHash: users.passwordHash,
121
- roleName: roles.name,
122
- })
123
- .from(sessions)
124
- .innerJoin(users, eq(sessions.userId, users.id))
125
- .leftJoin(userRoles, eq(users.id, userRoles.userId))
126
- .leftJoin(roles, eq(userRoles.roleId, roles.id))
127
- .where(eq(sessions.id, sessionId));
128
-
129
- if (rows.length === 0) return null;
130
- const first = rows[0]!;
131
- return {
132
- id: first.sessionId,
133
- userId: first.sessionUserId,
134
- expiresAt: first.sessionExpiresAt,
135
- createdAt: first.sessionCreatedAt,
136
- user: {
137
- id: first.userId,
138
- identifier: first.identifier,
139
- passwordHash: first.passwordHash,
140
- roles: rows.flatMap((row) => (row.roleName ? [row.roleName] : [])),
141
- },
142
- };
143
- },
144
-
145
- async delete(sessionId) {
146
- await db.delete(sessions).where(eq(sessions.id, sessionId));
147
- },
148
-
149
- async deleteAllForUser(userId) {
150
- await db.delete(sessions).where(eq(sessions.userId, userId));
151
- },
152
- },
153
- };
154
- }
@@ -1,125 +0,0 @@
1
- // sentri — auth config template (Drizzle)
2
- // Generated by: npx sentri generate drizzle
3
- //
4
- // Update validRoles to match the roles your application uses.
5
- // Add JWT_SECRET to your .env file.
6
- // If you already have a db instance defined elsewhere, import it instead.
7
-
8
- import { createAuth } from 'sentri';
9
- import { db } from '../db.js'; // adjust path to your db instance
10
- import { createAdapter } from './adapter.js';
11
-
12
- export const auth = createAuth({
13
- secret: process.env.JWT_SECRET!,
14
- validRoles: ['user', 'admin'] as const,
15
- adapter: createAdapter(db),
16
- accessExpiresIn: '5m', // short-lived; silent refresh happens automatically in protect()
17
- // refreshExpiresIn: '7d',
18
- // algorithm: 'HS256',
19
- // saltRounds: 12,
20
- // apiKey: process.env.REGISTER_API_KEY, // when set, POST /register requires X-Api-Key header
21
-
22
- // Access token stored in a non-httpOnly cookie — browser JS can read it via
23
- // document.cookie or getCurrentAccessToken(req). protect() reads from this
24
- // cookie automatically when no Authorization header is present.
25
- accessCookie: {
26
- secure: process.env.NODE_ENV === 'production',
27
- // name: 'access_token',
28
- // sameSite: 'strict',
29
- // path: '/',
30
- },
31
-
32
- // Refresh token stored in an httpOnly cookie — not readable by JS.
33
- cookie: {
34
- secure: process.env.NODE_ENV === 'production',
35
- // name: 'refresh_token',
36
- // httpOnly: true,
37
- // sameSite: 'strict',
38
- // path: '/',
39
- },
40
-
41
- // hooks: {
42
- // // Fired after every successful login — good for audit logs, notifications.
43
- // onLogin: async (user) => {
44
- // await auditLog.record('login', user.id);
45
- // },
46
- // // Fired after every failed login — good for rate limiting, alerting.
47
- // onFailedLogin: (identifier, error) => {
48
- // rateLimiter.hit(`login:${identifier}`);
49
- // },
50
- // // Fired after logout (single session or all sessions).
51
- // onLogout: async (userId) => {
52
- // await cache.invalidate(userId);
53
- // },
54
- // },
55
-
56
- // isTokenRevoked: async (sessionId) => {
57
- // // Optional: immediate revocation via Redis (called on every protected request).
58
- // // Keep this fast — a slow check negates the performance benefit of stateless JWTs.
59
- // return await redis.sismember('revoked_sessions', sessionId);
60
- // },
61
-
62
- // router: {
63
- // register: async (input) => {
64
- // // custom register logic — must return RegisterResult
65
- // },
66
- // login: async (input) => {
67
- // // custom login logic — must return AuthResult
68
- // },
69
- // refresh: async (refreshToken) => {
70
- // // custom refresh logic — must return RefreshResult
71
- // },
72
- // logout: async (refreshToken) => {
73
- // // custom logout logic
74
- // },
75
- // logoutAll: async (userId) => {
76
- // // custom logout-all logic
77
- // },
78
- // assignRoles: async (userId, roles) => {
79
- // // custom assignRoles logic — must return AssignRolesResult
80
- // },
81
- // },
82
- });
83
-
84
- // --- Express app setup ---
85
- //
86
- // import express from 'express';
87
- // import { SentriError, createIdempotencyMiddleware, getCurrentAccessToken } from 'sentri';
88
- //
89
- // const app = express();
90
- // app.use(express.json());
91
- //
92
- // // Optional: make create/update operations idempotent via X-Idempotency-Key header
93
- // app.use(createIdempotencyMiddleware());
94
- //
95
- // // Mount the auth router (POST /auth/register, /auth/login, etc.)
96
- // app.use('/auth', auth.router());
97
- //
98
- // // Your own routes — protect() reads token from Authorization header OR access_token cookie
99
- // app.get('/protected', auth.protect(), (req, res) => {
100
- // res.json(req.user);
101
- // });
102
- //
103
- // // Read the raw access token if needed (header → cookie fallback)
104
- // app.get('/debug', (req, res) => {
105
- // const token = auth.getCurrentAccessToken(req); // or: getCurrentAccessToken(req, config)
106
- // res.json({ hasToken: !!token });
107
- // });
108
- //
109
- // // Domain-specific error by extending SentriError
110
- // class NotFoundError extends SentriError {
111
- // constructor(resource: string) {
112
- // super('NOT_FOUND', `${resource} not found`, 404);
113
- // }
114
- // }
115
- //
116
- // app.get('/items/:id', auth.protect(), async (req, res) => {
117
- // const item = await db.query.items.findFirst({ where: (t, { eq }) => eq(t.id, req.params['id']) });
118
- // if (!item) throw new NotFoundError('Item');
119
- // res.json(item);
120
- // });
121
- //
122
- // // Mount AFTER all routes — catches SentriError from sentri AND your subclasses
123
- // app.use(auth.errorHandler());
124
- //
125
- // app.listen(3000);
@@ -1,47 +0,0 @@
1
- // sentri — Drizzle schema template (PostgreSQL / node-postgres)
2
- // Generated by: npx sentri generate drizzle
3
- //
4
- // Field mapping: library uses `identifier` internally, but your DB column
5
- // can be named anything — adjust the first argument of text() on the
6
- // identifier column to match your existing schema, or leave it as-is.
7
- //
8
- // text('email') — login via email
9
- // text('username') — login via username
10
- // text('phone') — login via phone number
11
-
12
- import { pgTable, primaryKey, text, timestamp } from 'drizzle-orm/pg-core';
13
-
14
- export const users = pgTable('users', {
15
- id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
16
- identifier: text('email').notNull().unique(),
17
- passwordHash: text('password_hash').notNull(),
18
- createdAt: timestamp('created_at').notNull().defaultNow(),
19
- updatedAt: timestamp('updated_at').notNull().$onUpdateFn(() => new Date()),
20
- });
21
-
22
- export const roles = pgTable('roles', {
23
- id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
24
- name: text('name').notNull().unique(),
25
- });
26
-
27
- export const userRoles = pgTable(
28
- 'user_roles',
29
- {
30
- userId: text('user_id')
31
- .notNull()
32
- .references(() => users.id, { onDelete: 'cascade' }),
33
- roleId: text('role_id')
34
- .notNull()
35
- .references(() => roles.id, { onDelete: 'cascade' }),
36
- },
37
- (table) => [primaryKey({ columns: [table.userId, table.roleId] })],
38
- );
39
-
40
- export const sessions = pgTable('sessions', {
41
- id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
42
- userId: text('user_id')
43
- .notNull()
44
- .references(() => users.id, { onDelete: 'cascade' }),
45
- expiresAt: timestamp('expires_at').notNull(),
46
- createdAt: timestamp('created_at').notNull().defaultNow(),
47
- });
@@ -1,122 +0,0 @@
1
- // sentri — Prisma adapter template
2
- // Generated by: npx sentri generate prisma
3
- //
4
- // Adjust the Prisma model/field names below if your schema differs from the
5
- // template produced by `npx sentri init`.
6
-
7
- import type { PrismaClient } from '@prisma/client';
8
- import { AuthError, type AuthAdapter } from 'sentri';
9
-
10
- export function createAdapter(prisma: PrismaClient): AuthAdapter {
11
- if (!prisma) {
12
- throw new AuthError(
13
- 'CONFIGURATION_ERROR',
14
- 'createAdapter requires a PrismaClient instance. Did you forget to pass it?\n' +
15
- 'Example: createAdapter(prisma)',
16
- );
17
- }
18
- return {
19
- user: {
20
- async findByIdentifier(identifier) {
21
- const user = await prisma.user.findUnique({
22
- where: { identifier },
23
- include: { userRoles: { include: { role: true } } },
24
- });
25
- if (!user) return null;
26
- return {
27
- id: user.id,
28
- identifier: user.identifier,
29
- passwordHash: user.passwordHash,
30
- roles: user.userRoles.map((userRole) => userRole.role.name),
31
- };
32
- },
33
-
34
- async findById(id) {
35
- const user = await prisma.user.findUnique({
36
- where: { id },
37
- include: { userRoles: { include: { role: true } } },
38
- });
39
- if (!user) return null;
40
- return {
41
- id: user.id,
42
- identifier: user.identifier,
43
- passwordHash: user.passwordHash,
44
- roles: user.userRoles.map((userRole) => userRole.role.name),
45
- };
46
- },
47
-
48
- async create({ identifier, passwordHash, roles }) {
49
- const user = await prisma.user.create({
50
- data: {
51
- identifier,
52
- passwordHash,
53
- userRoles: {
54
- create: await Promise.all(
55
- roles.map(async (name) => {
56
- const role = await prisma.role.upsert({
57
- where: { name },
58
- update: {},
59
- create: { name },
60
- });
61
- return { roleId: role.id };
62
- }),
63
- ),
64
- },
65
- },
66
- });
67
- return { id: user.id };
68
- },
69
-
70
- async updateRoles(userId, roles) {
71
- await prisma.userRole.deleteMany({ where: { userId } });
72
- await Promise.all(
73
- roles.map(async (name) => {
74
- const role = await prisma.role.upsert({
75
- where: { name },
76
- update: {},
77
- create: { name },
78
- });
79
- await prisma.userRole.create({ data: { userId, roleId: role.id } });
80
- }),
81
- );
82
- },
83
- },
84
-
85
- session: {
86
- async create({ userId, expiresAt }) {
87
- const session = await prisma.session.create({ data: { userId, expiresAt } });
88
- return { id: session.id };
89
- },
90
-
91
- async findById(sessionId) {
92
- const session = await prisma.session.findUnique({
93
- where: { id: sessionId },
94
- include: {
95
- user: { include: { userRoles: { include: { role: true } } } },
96
- },
97
- });
98
- if (!session) return null;
99
- return {
100
- id: session.id,
101
- userId: session.userId,
102
- expiresAt: session.expiresAt,
103
- createdAt: session.createdAt,
104
- user: {
105
- id: session.user.id,
106
- identifier: session.user.identifier,
107
- passwordHash: session.user.passwordHash,
108
- roles: session.user.userRoles.map((userRole) => userRole.role.name),
109
- },
110
- };
111
- },
112
-
113
- async delete(sessionId) {
114
- await prisma.session.delete({ where: { id: sessionId } }).catch(() => {});
115
- },
116
-
117
- async deleteAllForUser(userId) {
118
- await prisma.session.deleteMany({ where: { userId } });
119
- },
120
- },
121
- };
122
- }