@stacksjs/auth 0.70.354 → 0.70.356

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.
@@ -1,3 +1,3 @@
1
- import{Buffer}from"node:buffer";import{createHmac,randomBytes,timingSafeEqual}from"node:crypto";import{config}from"@stacksjs/config";import{db,sqlDateTime,parseSqlDateTime}from"@stacksjs/database";import{mail,template}from"@stacksjs/email";import{log}from"@stacksjs/logging";function getVerificationKey(){const appKey=config.app.key;if(typeof appKey!=="string"||appKey.length===0)throw Error("[auth] config.app.key is not set \u2014 email-verification HMAC requires a real APP_KEY. "+"Run `./buddy key:generate` to provision one, or set the APP_KEY env var before booting the app.");return appKey}function generateVerificationToken(userId){const nonce=randomBytes(32).toString("hex"),payload=`${userId}:${nonce}`,hash=createHmac("sha256",getVerificationKey()).update(payload).digest("hex");return{token:nonce,hash}}function verifyToken(userId,token,storedHash){const payload=`${userId}:${token}`,hash=createHmac("sha256",getVerificationKey()).update(payload).digest("hex"),a=Buffer.from(hash),b=Buffer.from(storedHash);if(a.length!==b.length)return!1;return timingSafeEqual(a,b)}function getExpiryMinutes(){const emailVerification=(config.auth??{}).emailVerification;if(emailVerification!=null&&typeof emailVerification==="object"){const ev=emailVerification;if(typeof ev.expire==="number")return ev.expire}return 60}function getVerificationUrl(userId,token){const base=config.app.url?`https://${config.app.url}`:`http://localhost:${process.env.PORT||"3000"}`,filled=(config.auth.emailVerification?.url??"/verify-email/{id}/{token}").replace("{id}",String(userId)).replace("{token}",token);return/^https?:\/\//.test(filled)?filled:`${base}${filled.startsWith("/")?"":"/"}${filled}`}export function isEmailVerified(user){return user.email_verified_at!=null}export async function sendVerificationEmail(user){const{token,hash}=generateVerificationToken(user.id),expiryMinutes=getExpiryMinutes(),expiresAt=new Date(Date.now()+expiryMinutes*60*1000);await db.deleteFrom("email_verifications").where("user_id","=",user.id).execute();await db.insertInto("email_verifications").values({user_id:user.id,token:hash,expires_at:sqlDateTime(expiresAt)}).executeTakeFirst();const verificationUrl=getVerificationUrl(user.id,token),appName=config.app.name||"Stacks";try{const{html,text}=await template("email-verification",{subject:`Verify Your ${appName} Email Address`,variables:{verificationUrl,expiryMinutes,userName:user.name||user.email}});if(!html&&!text)throw Error("email-verification template missing or rendered empty");await mail.send({to:user.email,subject:`Verify Your ${appName} Email Address`,text,html})}catch(templateError){const errorMessage=templateError instanceof Error?templateError.message:String(templateError);log.warn(`[email] Email verification template failed, using plain text fallback: ${errorMessage}`);await mail.send({to:user.email,subject:`Verify Your ${appName} Email Address`,text:`Please verify your email address by visiting: ${verificationUrl}
1
+ import{Buffer}from"node:buffer";import{createHmac,randomBytes,timingSafeEqual}from"node:crypto";import{config}from"@stacksjs/config";import{db,sqlDateTime,parseSqlDateTime}from"@stacksjs/database";import{mail,template}from"@stacksjs/email";import{log}from"@stacksjs/logging";function getVerificationKey(){const appKey=config.app.key;if(typeof appKey!=="string"||appKey.length===0)throw Error("[auth] config.app.key is not set \u2014 email-verification HMAC requires a real APP_KEY. "+"Run `./buddy key:generate` to provision one, or set the APP_KEY env var before booting the app.");return appKey}function generateVerificationToken(userId){const nonce=randomBytes(32).toString("hex"),payload=`${userId}:${nonce}`,hash=createHmac("sha256",getVerificationKey()).update(payload).digest("hex");return{token:nonce,hash}}function verifyToken(userId,token,storedHash){const payload=`${userId}:${token}`,hash=createHmac("sha256",getVerificationKey()).update(payload).digest("hex"),a=Buffer.from(hash),b=Buffer.from(storedHash);if(a.length!==b.length)return!1;return timingSafeEqual(a,b)}function getExpiryMinutes(){const emailVerification=(config.auth??{}).emailVerification;if(emailVerification!=null&&typeof emailVerification==="object"){const ev=emailVerification;if(typeof ev.expire==="number")return ev.expire}return 60}function getVerificationUrl(userId,token){const base=config.app.url?`https://${config.app.url}`:`http://localhost:${process.env.PORT||"3000"}`,filled=(config.auth.emailVerification?.url??"/verify-email/{id}/{token}").replace("{id}",String(userId)).replace("{token}",token);return/^https?:\/\//.test(filled)?filled:`${base}${filled.startsWith("/")?"":"/"}${filled}`}export function isEmailVerified(user){return user.email_verified_at!=null}export async function sendVerificationEmail(user){const{token,hash}=generateVerificationToken(user.id),expiryMinutes=getExpiryMinutes(),expiresAt=new Date(Date.now()+expiryMinutes*60*1000);await db.deleteFrom("email_verifications").where("user_id","=",user.id).execute();await db.insertInto("email_verifications").values({user_id:user.id,token:hash,expires_at:sqlDateTime(expiresAt)}).executeTakeFirst();const verificationUrl=getVerificationUrl(user.id,token),appName=config.app.name||"Stacks";let html,text;try{const rendered=await template("email-verification",{subject:`Verify Your ${appName} Email Address`,variables:{verificationUrl,expiryMinutes,userName:user.name||user.email}});html=rendered.html;text=rendered.text;if(!html&&!text)throw Error("email-verification template missing or rendered empty")}catch(templateError){const errorMessage=templateError instanceof Error?templateError.message:String(templateError);log.warn(`[email] Email verification template failed, using plain text fallback: ${errorMessage}`);html=void 0;text=`Please verify your email address by visiting: ${verificationUrl}
2
2
 
3
- This link expires in ${expiryMinutes} minutes.`})}}export async function verifyEmail(userId,token){const record=await db.selectFrom("email_verifications").where("user_id","=",userId).selectAll().executeTakeFirst();if(!record)return{success:!1,message:"No verification request found. Please request a new verification email."};const expiresAt=parseSqlDateTime(record.expires_at)??new Date(Number.NaN);if(Number.isNaN(expiresAt.getTime())||new Date>expiresAt){await db.deleteFrom("email_verifications").where("user_id","=",userId).execute();return{success:!1,message:"Verification link has expired. Please request a new one."}}if(!verifyToken(userId,token,record.token))return{success:!1,message:"Invalid verification link."};await db.updateTable("users").set({email_verified_at:sqlDateTime()}).where("id","=",userId).executeTakeFirst();await db.deleteFrom("email_verifications").where("user_id","=",userId).execute();return{success:!0,message:"Email verified successfully."}}export async function resendVerificationEmail(user){if(isEmailVerified(user))return{success:!1,message:"Email is already verified."};const existing=await db.selectFrom("email_verifications").where("user_id","=",user.id).selectAll().executeTakeFirst();if(existing){const createdAt=new Date(existing.created_at),secondsSince=(Date.now()-createdAt.getTime())/1000;if(Number.isNaN(secondsSince))return{success:!1,message:"Please wait a moment before requesting another verification email."};if(secondsSince<60)return{success:!1,message:`Please wait ${Math.ceil(60-secondsSince)} seconds before requesting another verification email.`}}await sendVerificationEmail(user);return{success:!0,message:"Verification email sent."}}export const EmailVerification={isVerified:isEmailVerified,send:sendVerificationEmail,verify:verifyEmail,resend:resendVerificationEmail};
3
+ This link expires in ${expiryMinutes} minutes.`}await mail.sendOrFail({to:user.email,subject:`Verify Your ${appName} Email Address`,text,html})}export async function verifyEmail(userId,token){const record=await db.selectFrom("email_verifications").where("user_id","=",userId).selectAll().executeTakeFirst();if(!record)return{success:!1,message:"No verification request found. Please request a new verification email."};const expiresAt=parseSqlDateTime(record.expires_at)??new Date(Number.NaN);if(Number.isNaN(expiresAt.getTime())||new Date>expiresAt){await db.deleteFrom("email_verifications").where("user_id","=",userId).execute();return{success:!1,message:"Verification link has expired. Please request a new one."}}if(!verifyToken(userId,token,record.token))return{success:!1,message:"Invalid verification link."};await db.updateTable("users").set({email_verified_at:sqlDateTime()}).where("id","=",userId).executeTakeFirst();await db.deleteFrom("email_verifications").where("user_id","=",userId).execute();return{success:!0,message:"Email verified successfully."}}export async function resendVerificationEmail(user){if(isEmailVerified(user))return{success:!1,message:"Email is already verified."};const existing=await db.selectFrom("email_verifications").where("user_id","=",user.id).selectAll().executeTakeFirst();if(existing){const createdAt=new Date(existing.created_at),secondsSince=(Date.now()-createdAt.getTime())/1000;if(Number.isNaN(secondsSince))return{success:!1,message:"Please wait a moment before requesting another verification email."};if(secondsSince<60)return{success:!1,message:`Please wait ${Math.ceil(60-secondsSince)} seconds before requesting another verification email.`}}await sendVerificationEmail(user);return{success:!0,message:"Verification email sent."}}export const EmailVerification={isVerified:isEmailVerified,send:sendVerificationEmail,verify:verifyEmail,resend:resendVerificationEmail};
@@ -1,3 +1,3 @@
1
- import{randomBytes}from"node:crypto";import{config}from"@stacksjs/config";import{db,sqlDateTime}from"@stacksjs/database";import{mail,template}from"@stacksjs/email";import{log}from"@stacksjs/logging";import{formatDate}from"@stacksjs/orm";import{makeHash,verifyHash}from"@stacksjs/security";import{sessionDestroyAll}from"../session-auth";import{revokeAllTokens}from"../tokens";function getTokenExpireMinutes(){return config.auth.passwordReset?.expire??60}function isWithinExpiry(row){const explicit=row.expires_at;if(typeof explicit==="string"||explicit instanceof Date)return new Date(explicit).getTime()>Date.now();const created=row.created_at;if(typeof created==="string"||created instanceof Date){const expireMinutes=getTokenExpireMinutes();return new Date(created).getTime()+expireMinutes*60000>Date.now()}return!1}async function sendPasswordChangedNotification(userEmail){const appName=config.app.name||"Stacks",supportEmail=config.app.supportEmail||config.email?.from?.address||"",changedAt=new Date().toLocaleString("en-US",{dateStyle:"full",timeStyle:"short"});try{const{html,text}=await template("password-changed",{subject:`Your ${appName} password has been changed`,variables:{changedAt,supportEmail}});if(!html&&!text){await mail.send({to:userEmail,subject:`Your ${appName} password has been changed`,text:`Your ${appName} password was changed on ${changedAt}.${supportEmail?` If this wasn't you, contact ${supportEmail}.`:""}`});return}await mail.send({to:userEmail,subject:`Your ${appName} password has been changed`,text,html})}catch(error){console.error("[PasswordReset] Failed to send password changed notification:",error)}}export function passwordResets(email){function generateResetToken(){return randomBytes(32).toString("hex")}async function createResetToken(){const token=generateResetToken(),hashedToken=await makeHash(token,{algorithm:"bcrypt"}),expireMinutes=getTokenExpireMinutes(),expiresAt=sqlDateTime(new Date(Date.now()+expireMinutes*60000));await db.deleteFrom("password_resets").where("email","=",email).execute();await db.insertInto("password_resets").values({email,token:hashedToken,expires_at:expiresAt}).executeTakeFirst();return token}async function sendEmail(){if(!await db.selectFrom("users").where("email","=",email).selectAll().executeTakeFirst())return;const token=await createResetToken(),expireMinutes=getTokenExpireMinutes(),appName=config.app.name||"Stacks",base=config.app.url?`https://${config.app.url}`:`http://localhost:${process.env.PORT||"3000"}`,filled=(config.auth.passwordReset?.url??"/password/reset/{token}?email={email}").replace("{token}",token).replace("{email}",encodeURIComponent(email)),resetUrl=/^https?:\/\//.test(filled)?filled:`${base}${filled.startsWith("/")?"":"/"}${filled}`;try{const{html,text}=await template("password-reset",{subject:`Reset Your ${appName} Password`,variables:{resetUrl,expireMinutes}});if(!html&&!text)throw Error("password-reset template missing or rendered empty");await mail.send({to:email,subject:`Reset Your ${appName} Password`,text,html})}catch(templateError){const msg=templateError instanceof Error?templateError.message:String(templateError);console.warn(`[PasswordReset] template render failed, sending plain-text fallback: ${msg}`);await mail.send({to:email,subject:`Reset Your ${appName} Password`,text:`Reset your password by visiting: ${resetUrl}
1
+ import{randomBytes}from"node:crypto";import{config}from"@stacksjs/config";import{db,sqlDateTime}from"@stacksjs/database";import{mail,template}from"@stacksjs/email";import{log}from"@stacksjs/logging";import{formatDate}from"@stacksjs/orm";import{makeHash,verifyHash}from"@stacksjs/security";import{sessionDestroyAll}from"../session-auth";import{revokeAllTokens}from"../tokens";function getTokenExpireMinutes(){return config.auth.passwordReset?.expire??60}function isWithinExpiry(row){const explicit=row.expires_at;if(typeof explicit==="string"||explicit instanceof Date)return new Date(explicit).getTime()>Date.now();const created=row.created_at;if(typeof created==="string"||created instanceof Date){const expireMinutes=getTokenExpireMinutes();return new Date(created).getTime()+expireMinutes*60000>Date.now()}return!1}async function sendPasswordChangedNotification(userEmail){const appName=config.app.name||"Stacks",supportEmail=config.app.supportEmail||config.email?.from?.address||"",changedAt=new Date().toLocaleString("en-US",{dateStyle:"full",timeStyle:"short"});try{const{html,text}=await template("password-changed",{subject:`Your ${appName} password has been changed`,variables:{changedAt,supportEmail}});if(!html&&!text){await mail.sendOrFail({to:userEmail,subject:`Your ${appName} password has been changed`,text:`Your ${appName} password was changed on ${changedAt}.${supportEmail?` If this wasn't you, contact ${supportEmail}.`:""}`});return}await mail.sendOrFail({to:userEmail,subject:`Your ${appName} password has been changed`,text,html})}catch(error){console.error("[PasswordReset] Failed to send password changed notification:",error)}}export function passwordResets(email){function generateResetToken(){return randomBytes(32).toString("hex")}async function createResetToken(){const token=generateResetToken(),hashedToken=await makeHash(token,{algorithm:"bcrypt"}),expireMinutes=getTokenExpireMinutes(),expiresAt=sqlDateTime(new Date(Date.now()+expireMinutes*60000));await db.deleteFrom("password_resets").where("email","=",email).execute();await db.insertInto("password_resets").values({email,token:hashedToken,expires_at:expiresAt}).executeTakeFirst();return token}async function sendEmail(){if(!await db.selectFrom("users").where("email","=",email).selectAll().executeTakeFirst())return;const token=await createResetToken(),expireMinutes=getTokenExpireMinutes(),appName=config.app.name||"Stacks",base=config.app.url?`https://${config.app.url}`:`http://localhost:${process.env.PORT||"3000"}`,filled=(config.auth.passwordReset?.url??"/password/reset/{token}?email={email}").replace("{token}",token).replace("{email}",encodeURIComponent(email)),resetUrl=/^https?:\/\//.test(filled)?filled:`${base}${filled.startsWith("/")?"":"/"}${filled}`;let html,text;try{const rendered=await template("password-reset",{subject:`Reset Your ${appName} Password`,variables:{resetUrl,expireMinutes}});html=rendered.html;text=rendered.text;if(!html&&!text)throw Error("password-reset template missing or rendered empty")}catch(templateError){const msg=templateError instanceof Error?templateError.message:String(templateError);console.warn(`[PasswordReset] template render failed, sending plain-text fallback: ${msg}`);html=void 0;text=`Reset your password by visiting: ${resetUrl}
2
2
 
3
- This link expires in ${expireMinutes} minutes. If you didn't request this, you can safely ignore this email.`})}}async function verifyToken(token){const result=await db.selectFrom("password_resets").where("email","=",email).selectAll().executeTakeFirst();if(!result)return!1;if(!isWithinExpiry(result)){await db.deleteFrom("password_resets").where("email","=",email).execute();return!1}const hashedToken=result.token;return await verifyHash(token,hashedToken)}async function resetPassword(token,newPassword){const result=await db.transaction(async(rawTrx)=>{const trx=rawTrx,resetRecord=await trx.selectFrom("password_resets").where("email","=",email).selectAll().executeTakeFirst();if(!resetRecord)return{success:!1,message:"Invalid or expired reset token"};if(!isWithinExpiry(resetRecord)){await trx.deleteFrom("password_resets").where("email","=",email).execute();return{success:!1,message:"This password reset link has expired. Please request a new one."}}const hashedToken=resetRecord.token;if(!await verifyHash(token,hashedToken))return{success:!1,message:"Invalid or expired reset token"};const user=await trx.selectFrom("users").where("email","=",email).selectAll().executeTakeFirst();if(!user)return{success:!1,message:"Invalid or expired reset token"};const hashedPassword=await makeHash(newPassword,{algorithm:"bcrypt"});try{await trx.updateTable("users").set({password:hashedPassword,password_changed_at:formatDate(new Date)}).where("email","=",email).executeTakeFirst()}catch(err){const message=err instanceof Error?err.message:String(err);if(/password_changed_at|no such column|unknown column/i.test(message)){log.warn("[PasswordReset] password_changed_at column missing \u2014 run `buddy migrate`; resetting without the credential-version stamp");await trx.updateTable("users").set({password:hashedPassword}).where("email","=",email).executeTakeFirst()}else throw err}await trx.deleteFrom("password_resets").where("email","=",email).execute();return{success:!0,userId:Number(user.id)}});if(result.success){await revokeAllTokens(result.userId);await sessionDestroyAll(result.userId);sendPasswordChangedNotification(email).catch((err)=>{console.error("[PasswordReset] Failed to send notification:",err)});return{success:!0}}return result}return{sendEmail,verifyToken,resetPassword}}
3
+ This link expires in ${expireMinutes} minutes. If you didn't request this, you can safely ignore this email.`}await mail.sendOrFail({to:email,subject:`Reset Your ${appName} Password`,text,html})}async function verifyToken(token){const result=await db.selectFrom("password_resets").where("email","=",email).selectAll().executeTakeFirst();if(!result)return!1;if(!isWithinExpiry(result)){await db.deleteFrom("password_resets").where("email","=",email).execute();return!1}const hashedToken=result.token;return await verifyHash(token,hashedToken)}async function resetPassword(token,newPassword){const result=await db.transaction(async(rawTrx)=>{const trx=rawTrx,resetRecord=await trx.selectFrom("password_resets").where("email","=",email).selectAll().executeTakeFirst();if(!resetRecord)return{success:!1,message:"Invalid or expired reset token"};if(!isWithinExpiry(resetRecord)){await trx.deleteFrom("password_resets").where("email","=",email).execute();return{success:!1,message:"This password reset link has expired. Please request a new one."}}const hashedToken=resetRecord.token;if(!await verifyHash(token,hashedToken))return{success:!1,message:"Invalid or expired reset token"};const user=await trx.selectFrom("users").where("email","=",email).selectAll().executeTakeFirst();if(!user)return{success:!1,message:"Invalid or expired reset token"};const hashedPassword=await makeHash(newPassword,{algorithm:"bcrypt"});try{await trx.updateTable("users").set({password:hashedPassword,password_changed_at:formatDate(new Date)}).where("email","=",email).executeTakeFirst()}catch(err){const message=err instanceof Error?err.message:String(err);if(/password_changed_at|no such column|unknown column/i.test(message)){log.warn("[PasswordReset] password_changed_at column missing \u2014 run `buddy migrate`; resetting without the credential-version stamp");await trx.updateTable("users").set({password:hashedPassword}).where("email","=",email).executeTakeFirst()}else throw err}await trx.deleteFrom("password_resets").where("email","=",email).execute();return{success:!0,userId:Number(user.id)}});if(result.success){await revokeAllTokens(result.userId);await sessionDestroyAll(result.userId);sendPasswordChangedNotification(email).catch((err)=>{console.error("[PasswordReset] Failed to send notification:",err)});return{success:!0}}return result}return{sendEmail,verifyToken,resetPassword}}
package/dist/rbac.d.ts CHANGED
@@ -251,6 +251,13 @@ export declare interface RbacMethods {
251
251
  // Use the row/instance shape from orm so role helpers operate on the
252
252
  // authenticated user object, not the User class constructor.
253
253
  declare type UserModel = OrmUserModel;
254
+ export type RbacEntity = 'permission' | 'role';
255
+ export declare class RbacEntityNotFoundError extends Error {
256
+ public readonly entity: RbacEntity;
257
+ public readonly value: string;
258
+ public readonly guardName: string;
259
+ constructor(entity: RbacEntity, value: string, guardName: string);
260
+ }
254
261
  /**
255
262
  * FIFO-bounded Map. Wraps Map with a hard size cap; on overflow, the
256
263
  * oldest entry (Map insertion order) is evicted. Used for the RBAC
package/dist/rbac.js CHANGED
@@ -1 +1 @@
1
- var {require}=import.meta;class BoundedMap{max;map=new Map;constructor(max){this.max=max}get(key){return this.map.get(key)}has(key){return this.map.has(key)}set(key,value){if(this.map.has(key))this.map.delete(key);this.map.set(key,value);if(this.map.size>this.max){const oldest=this.map.keys().next().value;if(oldest!==void 0)this.map.delete(oldest)}return this}delete(key){return this.map.delete(key)}clear(){this.map.clear()}}const RBAC_USER_CACHE_MAX=1e4,cache={userRoles:new BoundedMap(RBAC_USER_CACHE_MAX),userPermissions:new BoundedMap(RBAC_USER_CACHE_MAX),rolePermissions:new BoundedMap(RBAC_USER_CACHE_MAX),roles:new Map,permissions:new Map};let store=null;export function setRbacStore(rbacStore){store=rbacStore;flushRbacCache()}function getStore(){if(!store){const{createBqbRbacStore}=require("./rbac-store-bqb");store=createBqbRbacStore()}return store}export function flushRbacCache(){cache.userRoles.clear();cache.userPermissions.clear();cache.rolePermissions.clear();cache.roles.clear();cache.permissions.clear()}function getUserId(user){if(typeof user==="number"){if(!Number.isFinite(user)||user<=0)throw TypeError("RBAC user id must be a positive number");return user}const id=user.id;if(typeof id!=="number"||!Number.isFinite(id)||id<=0)throw TypeError(`RBAC user id must be a positive number, got ${typeof id} (${String(id)})`);return id}export async function createRole(name,guardName="web",description){const role=await getStore().createRole(name,guardName,description);cache.roles.set(`${name}:${guardName}`,role);return role}export async function findRole(name,guardName="web"){const cacheKey=`${name}:${guardName}`;if(cache.roles.has(cacheKey))return cache.roles.get(cacheKey);const role=await getStore().findRoleByName(name,guardName);if(role)cache.roles.set(cacheKey,role);return role}export async function deleteRole(name,guardName="web"){const role=await findRole(name,guardName);if(role){await getStore().deleteRole(role.id);cache.roles.delete(`${name}:${guardName}`);flushRbacCache()}}export async function getAllRoles(guardName){return getStore().getAllRoles(guardName)}export async function createPermission(name,guardName="web",description){const permission=await getStore().createPermission(name,guardName,description);cache.permissions.set(`${name}:${guardName}`,permission);return permission}export async function findPermission(name,guardName="web"){const cacheKey=`${name}:${guardName}`;if(cache.permissions.has(cacheKey))return cache.permissions.get(cacheKey);const permission=await getStore().findPermissionByName(name,guardName);if(permission)cache.permissions.set(cacheKey,permission);return permission}export async function deletePermission(name,guardName="web"){const permission=await findPermission(name,guardName);if(permission){await getStore().deletePermission(permission.id);cache.permissions.delete(`${name}:${guardName}`);flushRbacCache()}}export async function getAllPermissions(guardName){return getStore().getAllPermissions(guardName)}export async function getUserRoles(user){const userId=getUserId(user);if(cache.userRoles.has(userId))return cache.userRoles.get(userId);const roles=await getStore().getUserRoles(userId);cache.userRoles.set(userId,roles);return roles}export async function assignRole(user,roleName,guardName="web"){const userId=getUserId(user),role=await findRole(roleName,guardName);if(!role)throw Error(`Role '${roleName}' not found.`);await getStore().assignRoleToUser(userId,role.id);cache.userRoles.delete(userId);cache.userPermissions.delete(userId)}export async function removeRole(user,roleName,guardName="web"){const userId=getUserId(user),role=await findRole(roleName,guardName);if(!role)return;await getStore().removeRoleFromUser(userId,role.id);cache.userRoles.delete(userId);cache.userPermissions.delete(userId)}export async function removeAllRoles(user){const userId=getUserId(user);await getStore().removeAllRolesFromUser(userId);cache.userRoles.delete(userId);cache.userPermissions.delete(userId)}export async function syncRoles(user,roleNames,guardName="web"){const userId=getUserId(user),preservedRoleIds=(await getUserRoles(userId)).filter((role)=>role.guard_name!==guardName).map((role)=>role.id),roleIds=[];for(const name of new Set(roleNames)){const role=await findRole(name,guardName);if(!role)throw Error(`Role '${name}' not found.`);roleIds.push(role.id)}await getStore().syncUserRoles(userId,[...preservedRoleIds,...roleIds]);cache.userRoles.delete(userId);cache.userPermissions.delete(userId)}export async function hasRole(user,roleName,guardName="web"){return(await getUserRoles(user)).some((r)=>r.name===roleName&&r.guard_name===guardName)}export async function hasAnyRole(user,roleNames,guardName="web"){const roles=await getUserRoles(user);return roleNames.some((name)=>roles.some((r)=>r.name===name&&r.guard_name===guardName))}export async function hasAllRoles(user,roleNames,guardName="web"){const roles=await getUserRoles(user);return roleNames.every((name)=>roles.some((r)=>r.name===name&&r.guard_name===guardName))}export async function getUserPermissions(user){const userId=getUserId(user);if(cache.userPermissions.has(userId))return cache.userPermissions.get(userId);const directPermissions=await getStore().getUserDirectPermissions(userId),roles=await getUserRoles(user),rolePermissions=[];for(const role of roles){const perms=await getRolePermissions(role.id);rolePermissions.push(...perms)}const seen=new Set,allPermissions=[];for(const perm of[...directPermissions,...rolePermissions])if(!seen.has(perm.id)){seen.add(perm.id);allPermissions.push(perm)}cache.userPermissions.set(userId,allPermissions);return allPermissions}export async function givePermission(user,permissionName,guardName="web"){const userId=getUserId(user),permission=await findPermission(permissionName,guardName);if(!permission)throw Error(`Permission '${permissionName}' not found.`);await getStore().assignPermissionToUser(userId,permission.id);cache.userPermissions.delete(userId)}export async function revokePermission(user,permissionName,guardName="web"){const userId=getUserId(user),permission=await findPermission(permissionName,guardName);if(!permission)return;await getStore().removePermissionFromUser(userId,permission.id);cache.userPermissions.delete(userId)}export async function revokeAllPermissions(user){const userId=getUserId(user);await getStore().removeAllPermissionsFromUser(userId);cache.userPermissions.delete(userId)}export async function syncPermissions(user,permissionNames,guardName="web"){const userId=getUserId(user),preservedPermissionIds=(await getStore().getUserDirectPermissions(userId)).filter((permission)=>permission.guard_name!==guardName).map((permission)=>permission.id),permissionIds=[];for(const name of new Set(permissionNames)){const perm=await findPermission(name,guardName);if(!perm)throw Error(`Permission '${name}' not found.`);permissionIds.push(perm.id)}await getStore().syncUserPermissions(userId,[...preservedPermissionIds,...permissionIds]);cache.userPermissions.delete(userId)}export async function hasPermission(user,permissionName,guardName="web"){return(await getUserPermissions(user)).some((p)=>p.name===permissionName&&p.guard_name===guardName)}export async function hasAnyPermission(user,permissionNames,guardName="web"){const permissions=await getUserPermissions(user);return permissionNames.some((name)=>permissions.some((p)=>p.name===name&&p.guard_name===guardName))}export async function hasAllPermissions(user,permissionNames,guardName="web"){const permissions=await getUserPermissions(user);return permissionNames.every((name)=>permissions.some((p)=>p.name===name&&p.guard_name===guardName))}export async function getRolePermissions(roleId){if(cache.rolePermissions.has(roleId))return cache.rolePermissions.get(roleId);const permissions=await getStore().getRolePermissions(roleId);cache.rolePermissions.set(roleId,permissions);return permissions}export async function givePermissionToRole(roleName,permissionName,guardName="web"){const role=await findRole(roleName,guardName);if(!role)throw Error(`Role '${roleName}' not found.`);const permission=await findPermission(permissionName,guardName);if(!permission)throw Error(`Permission '${permissionName}' not found.`);await getStore().assignPermissionToRole(role.id,permission.id);cache.rolePermissions.delete(role.id);cache.userPermissions.clear()}export async function revokePermissionFromRole(roleName,permissionName,guardName="web"){const role=await findRole(roleName,guardName);if(!role)return;const permission=await findPermission(permissionName,guardName);if(!permission)return;await getStore().removePermissionFromRole(role.id,permission.id);cache.rolePermissions.delete(role.id);cache.userPermissions.clear()}export async function syncRolePermissions(roleName,permissionNames,guardName="web"){const role=await findRole(roleName,guardName);if(!role)throw Error(`Role '${roleName}' not found.`);const permissionIds=[];for(const name of permissionNames){const perm=await findPermission(name,guardName);if(!perm)throw Error(`Permission '${name}' not found.`);permissionIds.push(perm.id)}await getStore().syncRolePermissions(role.id,permissionIds);cache.rolePermissions.delete(role.id);cache.userPermissions.clear()}export function withRbac(user){const userId=getUserId(user);return Object.assign(user,{hasRole:(roleName,guardName)=>hasRole(userId,roleName,guardName),hasAnyRole:(roleNames,guardName)=>hasAnyRole(userId,roleNames,guardName),hasAllRoles:(roleNames,guardName)=>hasAllRoles(userId,roleNames,guardName),hasPermission:(permissionName,guardName)=>hasPermission(userId,permissionName,guardName),hasAnyPermission:(permissionNames,guardName)=>hasAnyPermission(userId,permissionNames,guardName),hasAllPermissions:(permissionNames,guardName)=>hasAllPermissions(userId,permissionNames,guardName),getRoles:()=>getUserRoles(userId),getPermissions:()=>getUserPermissions(userId),assignRole:(roleName,guardName)=>assignRole(userId,roleName,guardName),removeRole:(roleName,guardName)=>removeRole(userId,roleName,guardName),syncRoles:(roleNames,guardName)=>syncRoles(userId,roleNames,guardName),givePermission:(permissionName,guardName)=>givePermission(userId,permissionName,guardName),revokePermission:(permissionName,guardName)=>revokePermission(userId,permissionName,guardName),syncPermissions:(permissionNames,guardName)=>syncPermissions(userId,permissionNames,guardName)})}export const Rbac={setStore:setRbacStore,flushCache:flushRbacCache,createRole,findRole,deleteRole,getAllRoles,createPermission,findPermission,deletePermission,getAllPermissions,getUserRoles,assignRole,removeRole,removeAllRoles,syncRoles,hasRole,hasAnyRole,hasAllRoles,getUserPermissions,givePermission,revokePermission,revokeAllPermissions,syncPermissions,hasPermission,hasAnyPermission,hasAllPermissions,getRolePermissions,givePermissionToRole,revokePermissionFromRole,syncRolePermissions,withRbac};export default Rbac;
1
+ var {require}=import.meta;export class RbacEntityNotFoundError extends Error{entity;value;guardName;constructor(entity,value,guardName){const label=entity==="role"?"Role":"Permission";super(`${label} '${value}' not found.`);this.entity=entity;this.value=value;this.guardName=guardName;this.name="RbacEntityNotFoundError"}}class BoundedMap{max;map=new Map;constructor(max){this.max=max}get(key){return this.map.get(key)}has(key){return this.map.has(key)}set(key,value){if(this.map.has(key))this.map.delete(key);this.map.set(key,value);if(this.map.size>this.max){const oldest=this.map.keys().next().value;if(oldest!==void 0)this.map.delete(oldest)}return this}delete(key){return this.map.delete(key)}clear(){this.map.clear()}}const RBAC_USER_CACHE_MAX=1e4,cache={userRoles:new BoundedMap(RBAC_USER_CACHE_MAX),userPermissions:new BoundedMap(RBAC_USER_CACHE_MAX),rolePermissions:new BoundedMap(RBAC_USER_CACHE_MAX),roles:new Map,permissions:new Map};let store=null;export function setRbacStore(rbacStore){store=rbacStore;flushRbacCache()}function getStore(){if(!store){const{createBqbRbacStore}=require("./rbac-store-bqb");store=createBqbRbacStore()}return store}export function flushRbacCache(){cache.userRoles.clear();cache.userPermissions.clear();cache.rolePermissions.clear();cache.roles.clear();cache.permissions.clear()}function getUserId(user){if(typeof user==="number"){if(!Number.isFinite(user)||user<=0)throw TypeError("RBAC user id must be a positive number");return user}const id=user.id;if(typeof id!=="number"||!Number.isFinite(id)||id<=0)throw TypeError(`RBAC user id must be a positive number, got ${typeof id} (${String(id)})`);return id}export async function createRole(name,guardName="web",description){const role=await getStore().createRole(name,guardName,description);cache.roles.set(`${name}:${guardName}`,role);return role}export async function findRole(name,guardName="web"){const cacheKey=`${name}:${guardName}`;if(cache.roles.has(cacheKey))return cache.roles.get(cacheKey);const role=await getStore().findRoleByName(name,guardName);if(role)cache.roles.set(cacheKey,role);return role}export async function deleteRole(name,guardName="web"){const role=await findRole(name,guardName);if(role){await getStore().deleteRole(role.id);cache.roles.delete(`${name}:${guardName}`);flushRbacCache()}}export async function getAllRoles(guardName){return getStore().getAllRoles(guardName)}export async function createPermission(name,guardName="web",description){const permission=await getStore().createPermission(name,guardName,description);cache.permissions.set(`${name}:${guardName}`,permission);return permission}export async function findPermission(name,guardName="web"){const cacheKey=`${name}:${guardName}`;if(cache.permissions.has(cacheKey))return cache.permissions.get(cacheKey);const permission=await getStore().findPermissionByName(name,guardName);if(permission)cache.permissions.set(cacheKey,permission);return permission}export async function deletePermission(name,guardName="web"){const permission=await findPermission(name,guardName);if(permission){await getStore().deletePermission(permission.id);cache.permissions.delete(`${name}:${guardName}`);flushRbacCache()}}export async function getAllPermissions(guardName){return getStore().getAllPermissions(guardName)}export async function getUserRoles(user){const userId=getUserId(user);if(cache.userRoles.has(userId))return cache.userRoles.get(userId);const roles=await getStore().getUserRoles(userId);cache.userRoles.set(userId,roles);return roles}export async function assignRole(user,roleName,guardName="web"){const userId=getUserId(user),role=await findRole(roleName,guardName);if(!role)throw new RbacEntityNotFoundError("role",roleName,guardName);await getStore().assignRoleToUser(userId,role.id);cache.userRoles.delete(userId);cache.userPermissions.delete(userId)}export async function removeRole(user,roleName,guardName="web"){const userId=getUserId(user),role=await findRole(roleName,guardName);if(!role)return;await getStore().removeRoleFromUser(userId,role.id);cache.userRoles.delete(userId);cache.userPermissions.delete(userId)}export async function removeAllRoles(user){const userId=getUserId(user);await getStore().removeAllRolesFromUser(userId);cache.userRoles.delete(userId);cache.userPermissions.delete(userId)}export async function syncRoles(user,roleNames,guardName="web"){const userId=getUserId(user),preservedRoleIds=(await getUserRoles(userId)).filter((role)=>role.guard_name!==guardName).map((role)=>role.id),roleIds=[];for(const name of new Set(roleNames)){const role=await findRole(name,guardName);if(!role)throw new RbacEntityNotFoundError("role",name,guardName);roleIds.push(role.id)}await getStore().syncUserRoles(userId,[...preservedRoleIds,...roleIds]);cache.userRoles.delete(userId);cache.userPermissions.delete(userId)}export async function hasRole(user,roleName,guardName="web"){return(await getUserRoles(user)).some((r)=>r.name===roleName&&r.guard_name===guardName)}export async function hasAnyRole(user,roleNames,guardName="web"){const roles=await getUserRoles(user);return roleNames.some((name)=>roles.some((r)=>r.name===name&&r.guard_name===guardName))}export async function hasAllRoles(user,roleNames,guardName="web"){const roles=await getUserRoles(user);return roleNames.every((name)=>roles.some((r)=>r.name===name&&r.guard_name===guardName))}export async function getUserPermissions(user){const userId=getUserId(user);if(cache.userPermissions.has(userId))return cache.userPermissions.get(userId);const directPermissions=await getStore().getUserDirectPermissions(userId),roles=await getUserRoles(user),rolePermissions=[];for(const role of roles){const perms=await getRolePermissions(role.id);rolePermissions.push(...perms)}const seen=new Set,allPermissions=[];for(const perm of[...directPermissions,...rolePermissions])if(!seen.has(perm.id)){seen.add(perm.id);allPermissions.push(perm)}cache.userPermissions.set(userId,allPermissions);return allPermissions}export async function givePermission(user,permissionName,guardName="web"){const userId=getUserId(user),permission=await findPermission(permissionName,guardName);if(!permission)throw new RbacEntityNotFoundError("permission",permissionName,guardName);await getStore().assignPermissionToUser(userId,permission.id);cache.userPermissions.delete(userId)}export async function revokePermission(user,permissionName,guardName="web"){const userId=getUserId(user),permission=await findPermission(permissionName,guardName);if(!permission)return;await getStore().removePermissionFromUser(userId,permission.id);cache.userPermissions.delete(userId)}export async function revokeAllPermissions(user){const userId=getUserId(user);await getStore().removeAllPermissionsFromUser(userId);cache.userPermissions.delete(userId)}export async function syncPermissions(user,permissionNames,guardName="web"){const userId=getUserId(user),preservedPermissionIds=(await getStore().getUserDirectPermissions(userId)).filter((permission)=>permission.guard_name!==guardName).map((permission)=>permission.id),permissionIds=[];for(const name of new Set(permissionNames)){const perm=await findPermission(name,guardName);if(!perm)throw new RbacEntityNotFoundError("permission",name,guardName);permissionIds.push(perm.id)}await getStore().syncUserPermissions(userId,[...preservedPermissionIds,...permissionIds]);cache.userPermissions.delete(userId)}export async function hasPermission(user,permissionName,guardName="web"){return(await getUserPermissions(user)).some((p)=>p.name===permissionName&&p.guard_name===guardName)}export async function hasAnyPermission(user,permissionNames,guardName="web"){const permissions=await getUserPermissions(user);return permissionNames.some((name)=>permissions.some((p)=>p.name===name&&p.guard_name===guardName))}export async function hasAllPermissions(user,permissionNames,guardName="web"){const permissions=await getUserPermissions(user);return permissionNames.every((name)=>permissions.some((p)=>p.name===name&&p.guard_name===guardName))}export async function getRolePermissions(roleId){if(cache.rolePermissions.has(roleId))return cache.rolePermissions.get(roleId);const permissions=await getStore().getRolePermissions(roleId);cache.rolePermissions.set(roleId,permissions);return permissions}export async function givePermissionToRole(roleName,permissionName,guardName="web"){const role=await findRole(roleName,guardName);if(!role)throw new RbacEntityNotFoundError("role",roleName,guardName);const permission=await findPermission(permissionName,guardName);if(!permission)throw new RbacEntityNotFoundError("permission",permissionName,guardName);await getStore().assignPermissionToRole(role.id,permission.id);cache.rolePermissions.delete(role.id);cache.userPermissions.clear()}export async function revokePermissionFromRole(roleName,permissionName,guardName="web"){const role=await findRole(roleName,guardName);if(!role)return;const permission=await findPermission(permissionName,guardName);if(!permission)return;await getStore().removePermissionFromRole(role.id,permission.id);cache.rolePermissions.delete(role.id);cache.userPermissions.clear()}export async function syncRolePermissions(roleName,permissionNames,guardName="web"){const role=await findRole(roleName,guardName);if(!role)throw new RbacEntityNotFoundError("role",roleName,guardName);const permissionIds=[];for(const name of permissionNames){const perm=await findPermission(name,guardName);if(!perm)throw new RbacEntityNotFoundError("permission",name,guardName);permissionIds.push(perm.id)}await getStore().syncRolePermissions(role.id,permissionIds);cache.rolePermissions.delete(role.id);cache.userPermissions.clear()}export function withRbac(user){const userId=getUserId(user);return Object.assign(user,{hasRole:(roleName,guardName)=>hasRole(userId,roleName,guardName),hasAnyRole:(roleNames,guardName)=>hasAnyRole(userId,roleNames,guardName),hasAllRoles:(roleNames,guardName)=>hasAllRoles(userId,roleNames,guardName),hasPermission:(permissionName,guardName)=>hasPermission(userId,permissionName,guardName),hasAnyPermission:(permissionNames,guardName)=>hasAnyPermission(userId,permissionNames,guardName),hasAllPermissions:(permissionNames,guardName)=>hasAllPermissions(userId,permissionNames,guardName),getRoles:()=>getUserRoles(userId),getPermissions:()=>getUserPermissions(userId),assignRole:(roleName,guardName)=>assignRole(userId,roleName,guardName),removeRole:(roleName,guardName)=>removeRole(userId,roleName,guardName),syncRoles:(roleNames,guardName)=>syncRoles(userId,roleNames,guardName),givePermission:(permissionName,guardName)=>givePermission(userId,permissionName,guardName),revokePermission:(permissionName,guardName)=>revokePermission(userId,permissionName,guardName),syncPermissions:(permissionNames,guardName)=>syncPermissions(userId,permissionNames,guardName)})}export const Rbac={setStore:setRbacStore,flushCache:flushRbacCache,createRole,findRole,deleteRole,getAllRoles,createPermission,findPermission,deletePermission,getAllPermissions,getUserRoles,assignRole,removeRole,removeAllRoles,syncRoles,hasRole,hasAnyRole,hasAllRoles,getUserPermissions,givePermission,revokePermission,revokeAllPermissions,syncPermissions,hasPermission,hasAnyPermission,hasAllPermissions,getRolePermissions,givePermissionToRole,revokePermissionFromRole,syncRolePermissions,withRbac};export default Rbac;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/auth",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.354",
5
+ "version": "0.70.356",
6
6
  "description": "A more simplistic way to authenticate.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -61,7 +61,7 @@
61
61
  },
62
62
  "devDependencies": {
63
63
  "better-dx": "^0.2.17",
64
- "@stacksjs/error-handling": "0.70.354",
65
- "@stacksjs/router": "0.70.354"
64
+ "@stacksjs/error-handling": "0.70.356",
65
+ "@stacksjs/router": "0.70.356"
66
66
  }
67
67
  }