@alepha/security
Version:
Manage realms, roles, permissions, and JWT-based authentication.
877 lines (861 loc) • 28.7 kB
JavaScript
import { $cursor, $env, $hook, $inject, $module, Alepha, AlephaError, AppNotStartedError, ContainerLockedError, Descriptor, KIND, createDescriptor, t } from "@alepha/core";
import { $logger } from "@alepha/logger";
import { createSecretKey, randomBytes, scrypt, timingSafeEqual } from "node:crypto";
import { DateTimeProvider } from "@alepha/datetime";
import { SignJWT, createLocalJWKSet, createRemoteJWKSet, jwtVerify } from "jose";
import { JWTClaimValidationFailed, JWTExpired } from "jose/errors";
import { promisify } from "node:util";
//#region src/errors/InvalidPermissionError.ts
var InvalidPermissionError = class extends Error {
constructor(name) {
super(`Permission '${name}' is invalid`);
}
};
//#endregion
//#region src/errors/InvalidTokenError.ts
var InvalidTokenError = class extends Error {
status = 401;
};
//#endregion
//#region src/errors/RealmNotFoundError.ts
var RealmNotFoundError = class extends Error {
constructor(realm) {
super(`Realm '${realm}' not found`);
}
};
//#endregion
//#region src/errors/SecurityError.ts
var SecurityError = class extends Error {
name = "SecurityError";
status = 403;
};
//#endregion
//#region src/providers/JwtProvider.ts
/**
* Provides utilities for working with JSON Web Tokens (JWT).
*/
var JwtProvider = class {
log = $logger();
keystore = [];
dateTimeProvider = $inject(DateTimeProvider);
encoder = new TextEncoder();
/**
* Adds a key loader to the embedded keystore.
*
* @param name
* @param secretKeyOrJwks
*/
setKeyLoader(name, secretKeyOrJwks) {
if (typeof secretKeyOrJwks === "object") {
this.log.info(`will verify JWTs from '${name}' with JWKS object (x${secretKeyOrJwks.keys.length})`);
this.keystore.push({
name,
keyLoader: createLocalJWKSet(secretKeyOrJwks)
});
} else if (this.isSecretKey(secretKeyOrJwks)) {
const secretKey = this.encoder.encode(secretKeyOrJwks);
this.log.info(`will verify JWTs from '${name}' with secret a key (${secretKey.length} bytes)`);
this.keystore.push({
name,
secretKey: secretKeyOrJwks,
keyLoader: () => Promise.resolve(createSecretKey(secretKey))
});
} else {
this.log.info(`will verify JWTs from '${name}' with JWKS ${secretKeyOrJwks}`);
this.keystore.push({
name,
keyLoader: createRemoteJWKSet(new URL(secretKeyOrJwks))
});
}
}
/**
* Retrieves the payload from a JSON Web Token (JWT).
*
* @param token - The JWT to extract the payload from.
*
* @return A Promise that resolves with the payload object from the token.
*/
async parse(token, keyName, options) {
for (const it of this.keystore) {
if (keyName && it.name !== keyName) continue;
this.log.trace(`Trying to verify token`, {
keyName: it.name,
options
});
try {
const verified = {
keyName: it.name,
result: await jwtVerify(token, it.keyLoader, {
currentDate: this.dateTimeProvider.now().toDate(),
...options
})
};
this.log.trace("Token verified successfully", { keyName: verified.keyName });
return verified;
} catch (error) {
this.log.trace("Token verification has failed", error);
if (error instanceof JWTExpired) throw new SecurityError("Token expired", { cause: error });
if (error instanceof JWTClaimValidationFailed) throw new SecurityError("Token claim validation failed", { cause: error });
}
}
this.log.warn(`No valid key loader found to verify the token (keystore size: ${this.keystore.length})`);
throw new SecurityError("Invalid token");
}
/**
* Creates a JWT token with the provided payload and secret key.
*
* @param payload - The payload to be encoded in the token.
* It should include the `realm_access` property which contains an array of roles.
* @param keyName - The name of the key to use when signing the token.
*
* @returns The signed JWT token.
*/
async create(payload, keyName, signOptions) {
const secretKey = keyName ? this.keystore.find((it) => it.name === keyName)?.secretKey : this.keystore[0]?.secretKey;
if (!secretKey) throw new AlephaError("No secret key found in the keystore");
const signJwt = new SignJWT(payload);
signJwt.setProtectedHeader({
alg: "HS256",
...signOptions?.header
});
return await signJwt.sign(this.encoder.encode(secretKey));
}
/**
* Determines if the provided key is a secret key.
*
* @param key
* @protected
*/
isSecretKey(key) {
return !key.startsWith("http");
}
};
//#endregion
//#region src/providers/SecurityProvider.ts
const DEFAULT_APP_SECRET = "05759934015388327323179852515731";
const envSchema = t.object({ APP_SECRET: t.string({ default: DEFAULT_APP_SECRET }) });
var SecurityProvider = class {
UNKNOWN_USER_NAME = "Anonymous User";
PERMISSION_REGEXP = /^[\w-]+(:[\w-]+)?$/;
PERMISSION_REGEXP_WILDCARD = /^[\w-]+(:[\w-]+)?|(:\*)$/;
log = $logger();
jwt = $inject(JwtProvider);
env = $env(envSchema);
alepha = $inject(Alepha);
get secretKey() {
return this.env.APP_SECRET;
}
/**
* The permissions configured for the security provider.
*/
permissions = [];
/**
* The realms configured for the security provider.
*/
realms = [{
name: "default",
secret: this.env.APP_SECRET,
roles: [{
name: "admin",
permissions: [{ name: "*" }]
}]
}];
start = $hook({
on: "start",
handler: async () => {
if (this.alepha.isProduction() && this.secretKey === DEFAULT_APP_SECRET) this.log.warn("Using default APP_SECRET in production is not recommended. Please set a strong APP_SECRET value.");
for (const realm of this.realms) if (realm.secret) {
const secret = typeof realm.secret === "function" ? realm.secret() : realm.secret;
this.jwt.setKeyLoader(realm.name, secret);
}
}
});
/**
* Adds a role to one or more realms.
*
* @param role
* @param realms
*/
createRole(role, ...realms) {
const list = realms.length ? realms.map((it) => {
const item = this.realms.find((realm) => realm.name === it);
if (!item) throw new RealmNotFoundError(it);
return item;
}) : this.realms;
for (const realm of list) {
for (const { name } of role.permissions) if (this.alepha.isStarted()) {
const parts = name.split(":");
const existing = this.permissions.find((it) => parts[0] === it.group && parts[1] === "*" || this.permissionToString(it) === name);
if (!existing) throw new SecurityError(`Permission '${name}' not found`);
} else if (name !== "*" && !this.PERMISSION_REGEXP_WILDCARD.test(name)) throw new InvalidPermissionError(name);
realm.roles.push(role);
}
return role;
}
/**
* Adds a permission to the security provider.
*
* @param raw - The permission to add.
*/
createPermission(raw) {
if (this.alepha.isStarted()) throw new ContainerLockedError();
let permission;
if (typeof raw === "string") {
if (!this.PERMISSION_REGEXP.test(raw)) throw new InvalidPermissionError(raw);
const parts = raw.split(":");
if (!parts[1]) permission = { name: parts[0] };
else permission = {
group: parts[0],
name: parts[1]
};
} else permission = raw;
const asString = this.permissionToString(permission);
if (!this.PERMISSION_REGEXP.test(asString)) throw new InvalidPermissionError(asString);
const existing = this.permissions.find((it) => this.permissionToString(it) === asString);
if (existing) {
this.log.warn(`Permission '${asString}' already exists. Skipping.`, {
current: existing,
new: permission
});
return existing;
}
this.log.trace(`Creating permission '${asString}'`);
this.permissions.push(permission);
return permission;
}
createRealm(realm) {
if (this.realms.length === 1 && this.realms[0].name === "default") this.realms.pop();
this.realms.push(realm);
}
/**
* Updates the roles for a realm then synchronizes the user account provider if available.
*
* Only available when the app is started.
*
* @param realm - The realm to update the roles for.
* @param roles - The roles to update.
*/
async updateRealm(realm, roles) {
if (!this.alepha.isStarted()) throw new AppNotStartedError();
const realmInstance = this.realms.find((it) => it.name === realm);
if (!realmInstance) throw new RealmNotFoundError(realm);
realmInstance.roles = roles;
}
/**
* Creates a user account from the provided payload.
*
* @param payload - The payload to create the user account from.
* @param [realmName] - The realm containing the roles. Default is all.
*
* @returns The user info created from the payload.
*/
createUserFromPayload(payload, realmName) {
const id = this.getIdFromPayload(payload);
const sessionId = this.getSessionIdFromPayload(payload);
const rolesFromPayload = this.getRolesFromPayload(payload);
const email = this.getEmailFromPayload(payload);
const username = this.getUsernameFromPayload(payload);
const picture = this.getPictureFromPayload(payload);
const name = this.getNameFromPayload(payload);
const organizations = this.getOrganizationsFromPayload(payload);
const rolesFromSystem = this.getRoles(realmName);
const roles = rolesFromPayload.reduce((arr, roleName) => arr.concat(rolesFromSystem.filter((it) => it.name === roleName)), []).map((it) => it.name);
const realm = this.realms.find((it) => it.name === realmName);
if (realm?.profile) return realm.profile(payload);
return {
id,
roles,
name,
email,
username,
picture,
organizations,
sessionId
};
}
/**
* Checks if the user has the specified permission.
*
* Bonus: we check also if the user has "ownership" flag.
*
* @param permissionLike - The permission to check for.
* @param roleEntries - The roles to check for the permission.
*/
checkPermission(permissionLike, ...roleEntries) {
const roles = roleEntries.map((it) => {
const role = this.getRoles().find((role$1) => role$1.name === it);
if (!role) throw new SecurityError(`Role '${it}' not found`);
return role;
});
const permission = this.permissionToString(permissionLike);
const isAdmin = roles.find((it) => it.permissions.find((it$1) => it$1.name === "*" && !it$1.exclude && !it$1.ownership));
if (isAdmin) return {
isAuthorized: true,
ownership: false
};
const result = {
isAuthorized: false,
ownership: void 0
};
const [group] = permission.split(":");
const groupWildcard = `${group}:*`;
for (const role of roles) for (const rolePermission of role.permissions) if (rolePermission.name === "*" || rolePermission.name === groupWildcard || rolePermission.name === permission) {
if (rolePermission.exclude?.includes(permission)) continue;
result.isAuthorized = true;
if (rolePermission.ownership) result.ownership = rolePermission.ownership;
else {
result.ownership = false;
return result;
}
}
return result;
}
/**
* Creates a user account from the provided payload.
*
* @param headerOrToken
* @param permissionLike
*/
async createUserFromToken(headerOrToken, options = {}) {
const token = headerOrToken?.replace("Bearer", "").trim();
if (typeof token !== "string" || token === "") throw new InvalidTokenError("Invalid authorization header, maybe token is missing ?");
const { result, keyName: realm } = await this.jwt.parse(token, options.realm, options.verify);
const info = this.createUserFromPayload(result.payload, realm);
const realmRoles = this.getRoles(realm).filter((it) => it.default);
const roles = info.roles ?? [];
for (const role of realmRoles) if (!roles.includes(role.name)) roles.push(role.name);
info.roles = roles;
await this.alepha.events.emit("security:user:created", {
realm,
user: info
});
let ownership;
if (options.permission) {
const check = this.checkPermission(options.permission, ...roles);
if (!check.isAuthorized) throw new SecurityError(`User is not allowed to access '${this.permissionToString(options.permission)}'`);
ownership = check.ownership;
}
return {
...info,
ownership,
token,
realm
};
}
/**
* Checks if a user has a specific role.
*
* @param roleName - The role to check for.
* @param permission - The permission to check for.
* @returns True if the user has the role, false otherwise.
*/
can(roleName, permission) {
return this.checkPermission(permission, roleName).isAuthorized;
}
/**
* Checks if a user has ownership of a specific permission.
*/
ownership(roleName, permission) {
return this.checkPermission(permission, roleName).ownership;
}
/**
* Converts a permission object to a string.
*
* @param permission
*/
permissionToString(permission) {
if (typeof permission === "string") return permission;
if (!permission.group) return permission.name;
return `${permission.group}:${permission.name}`;
}
getRealms() {
return this.realms;
}
/**
* Retrieves the user account from the provided user ID.
*
* @param realm
*/
getRoles(realm) {
if (realm) return [...this.realms.find((it) => it.name === realm)?.roles ?? []];
return this.realms.reduce((arr, it) => arr.concat(it.roles), []);
}
/**
* Returns all permissions.
*
* @param user - Filter permissions by user.
*
* @return An array containing all permissions.
*/
getPermissions(user) {
if (user?.roles) {
const permissions = [];
const roles = user.roles ?? [];
for (const roleOrString of roles) {
const role = typeof roleOrString === "string" ? this.getRoles(user.realm).find((it) => it.name === roleOrString) : roleOrString;
if (!role) throw new SecurityError(`Role '${roleOrString}' not found`);
if (role.permissions.some((it) => it.name === "*" && !it.exclude)) return this.getPermissions();
for (const permission of role.permissions) {
let ref = [];
if (permission.name === "*") ref.push(...this.permissions);
else if (permission.name.includes(":")) {
const [group, name] = permission.name.split(":");
if (name === "*") ref.push(...this.permissions.filter((it) => it.group === group));
else ref.push(...this.permissions.filter((it) => it.name === name && it.group === group));
} else ref.push(...this.permissions.filter((it) => it.name === permission.name && !it.group));
const exclude = permission.exclude;
if (exclude) ref = ref.filter((it) => !exclude.includes(this.permissionToString(it)));
permissions.push(...ref);
}
}
return [...new Set(permissions.filter((it) => it != null))];
}
return this.permissions;
}
/**
* Retrieves the user ID from the provided payload object.
*
* @param payload - The payload object from which to extract the user ID.
* @return The user ID as a string.
*/
getIdFromPayload(payload) {
if (payload.sub != null) return String(payload.sub);
if (payload.id != null) return String(payload.id);
if (payload.userId != null) return String(payload.userId);
throw new SecurityError("Invalid JWT - missing id");
}
getSessionIdFromPayload(payload) {
if (!payload) return;
if (payload.sid) return String(payload.sid);
}
/**
* Retrieves the roles from the provided payload object.
* @param payload - The payload object from which to extract the roles.
* @return An array of role strings.
*/
getRolesFromPayload(payload) {
return payload?.realm_access?.roles ?? payload?.roles ?? [];
}
getPictureFromPayload(payload) {
if (!payload) return;
if (payload.picture) return payload.picture;
if (payload.avatar_url) return payload.avatar_url;
if (payload.user_picture) return payload.user_picture;
return void 0;
}
getUsernameFromPayload(payload) {
if (!payload) return;
if (payload.preferred_username) return payload.preferred_username;
if (payload.username) return payload.username;
return void 0;
}
getEmailFromPayload(payload) {
if (!payload) return;
if (payload.email) return payload.email;
return void 0;
}
/**
* Returns the name from the given payload.
*
* @param payload - The payload object.
* @returns The name extracted from the payload, or an empty string if the payload is falsy or no name is found.
*/
getNameFromPayload(payload) {
if (!payload) return this.UNKNOWN_USER_NAME;
if (payload.name) return payload.name;
if (typeof payload.given_name === "string" && typeof payload.family_name === "string") return `${payload.given_name} ${payload.family_name}`.trim();
return this.UNKNOWN_USER_NAME;
}
getOrganizationsFromPayload(payload) {
if (!payload) return;
if (payload.organization) {
if (typeof payload.organization === "string") return [payload.organization];
if (Array.isArray(payload.organization)) return payload.organization;
}
}
};
//#endregion
//#region src/descriptors/$permission.ts
/**
* Create a new permission.
*/
const $permission = (options = {}) => {
return createDescriptor(PermissionDescriptor, options);
};
var PermissionDescriptor = class extends Descriptor {
securityProvider = $inject(SecurityProvider);
get name() {
return this.options.name || this.config.propertyKey;
}
get group() {
return this.options.group || this.config.service.name;
}
onInit() {
this.securityProvider.createPermission({
name: this.name,
group: this.group,
description: this.options.description
});
}
/**
* Check if the user has the permission.
*/
can(user) {
if (!user.roles) return false;
const check = this.securityProvider.checkPermission(this, ...user.roles);
return check.isAuthorized;
}
};
$permission[KIND] = PermissionDescriptor;
//#endregion
//#region src/descriptors/$realm.ts
/**
* Create a new realm.
*/
const $realm = (options) => {
return createDescriptor(RealmDescriptor, options);
};
var RealmDescriptor = class extends Descriptor {
securityProvider = $inject(SecurityProvider);
dateTimeProvider = $inject(DateTimeProvider);
jwt = $inject(JwtProvider);
log = $logger();
get name() {
return this.options.name || this.config.propertyKey;
}
get accessTokenExpiration() {
return this.dateTimeProvider.duration(this.options.settings?.accessToken?.expiration ?? [15, "minutes"]);
}
get refreshTokenExpiration() {
return this.dateTimeProvider.duration(this.options.settings?.refreshToken?.expiration ?? [30, "days"]);
}
onInit() {
const roles = this.options.roles?.map((it) => {
if (typeof it === "string") {
const role = this.getRoles().find((role$1) => role$1.name === it);
if (!role) throw new SecurityError(`Role '${it}' not found`);
return role;
}
return it;
}) ?? [];
this.securityProvider.createRealm({
name: this.name,
profile: this.options.profile,
secret: "jwks" in this.options ? this.options.jwks : this.options.secret,
roles
});
}
/**
* Get all roles in the realm.
*/
getRoles() {
return this.securityProvider.getRoles(this.name);
}
/**
* Set all roles in the realm.
*/
async setRoles(roles) {
await this.securityProvider.updateRealm(this.name, roles);
}
/**
* Get a role by name, throws an error if not found.
*/
getRoleByName(name) {
const role = this.getRoles().find((it) => it.name === name);
if (!role) throw new SecurityError(`Role '${name}' not found`);
return role;
}
async parseToken(token) {
const { result } = await this.jwt.parse(token, this.name);
return result.payload;
}
/**
* Create a token for the subject.
*/
async createToken(user, refreshToken) {
let sid = refreshToken?.sid;
let refresh_token = refreshToken?.refresh_token;
let refresh_token_expires_in = refreshToken?.refresh_token_expires_in;
const iat = this.dateTimeProvider.now().unix();
const exp = iat + this.accessTokenExpiration.asSeconds();
if (!refreshToken) {
const create = this.options.settings?.onCreateSession;
if (create) {
const expiresIn = this.refreshTokenExpiration.asSeconds();
const { refreshToken: refreshToken$1, sessionId } = await create(user, { expiresIn });
refresh_token = refreshToken$1;
refresh_token_expires_in = expiresIn;
sid = sessionId;
} else {
const payload = {
sub: user.id,
exp: iat + this.refreshTokenExpiration.asSeconds(),
iat,
aud: this.name
};
this.log.trace("Creating refresh token", payload);
sid = crypto.randomUUID();
refresh_token_expires_in = this.refreshTokenExpiration.asSeconds();
refresh_token = await this.jwt.create(payload, this.name, { header: { typ: "refresh" } });
}
}
this.log.trace("Creating access token", {
sub: user.id,
exp,
iat,
aud: this.name
});
const access_token = await this.jwt.create({
sub: user.id,
exp,
iat,
aud: this.name,
sid,
name: user.name,
email: user.email,
preferred_username: user.username,
picture: user.picture,
organizations: user.organizations,
roles: user.roles
}, this.name);
const response = {
access_token,
token_type: "Bearer",
expires_in: this.accessTokenExpiration.asSeconds(),
issued_at: iat,
refresh_token,
refresh_token_expires_in
};
return response;
}
async refreshToken(refreshToken, accessToken) {
if (this.options.settings?.onRefreshSession) {
const { user: user$1, expiresIn: expiresIn$1, sessionId } = await this.options.settings.onRefreshSession(refreshToken);
const tokens = await this.createToken(user$1, {
sid: sessionId,
refresh_token: refreshToken,
refresh_token_expires_in: expiresIn$1
});
return {
user: user$1,
tokens
};
}
if (!accessToken) throw new AlephaError("An access token is required for refreshing");
const user = await this.securityProvider.createUserFromToken(accessToken, {
realm: this.name,
verify: { currentDate: /* @__PURE__ */ new Date(0) }
});
const { result: { payload } } = await this.jwt.parse(refreshToken, this.name, {
typ: "refresh",
audience: this.name,
subject: user.id
});
const iat = this.dateTimeProvider.now().unix();
const expiresIn = payload.exp ? payload.exp - iat : this.refreshTokenExpiration.asSeconds();
return {
user,
tokens: await this.createToken(user, {
sid: payload.sid,
refresh_token: refreshToken,
refresh_token_expires_in: expiresIn
})
};
}
};
$realm[KIND] = RealmDescriptor;
//#endregion
//#region src/descriptors/$role.ts
/**
* Create a new role.
*/
const $role = (options = {}) => {
return createDescriptor(RoleDescriptor, options);
};
var RoleDescriptor = class extends Descriptor {
securityProvider = $inject(SecurityProvider);
get name() {
return this.options.name || this.config.propertyKey;
}
onInit() {
this.securityProvider.createRole({
...this.options,
name: this.name,
permissions: this.options.permissions?.map((it) => {
if (typeof it === "string") return { name: it };
return it;
}) ?? []
});
}
/**
* Get the realm of the role.
*/
get realm() {
return this.options.realm;
}
};
$role[KIND] = RoleDescriptor;
//#endregion
//#region src/providers/CryptoProvider.ts
const scryptAsync = promisify(scrypt);
var CryptoProvider = class {
async hashPassword(password) {
const salt = randomBytes(16).toString("hex");
const derivedKey = await scryptAsync(password, salt, 64);
return `${salt}:${derivedKey.toString("hex")}`;
}
async verifyPassword(password, stored) {
const [salt, originalHex] = stored.split(":");
const derivedKey = await scryptAsync(password, salt, 64);
const originalKey = Buffer.from(originalHex, "hex");
return timingSafeEqual(derivedKey, originalKey);
}
};
//#endregion
//#region src/descriptors/$serviceAccount.ts
/**
* Allow to get an access token for a service account.
*
* You have some options to configure the service account:
* - a OAUTH2 URL using client credentials grant type
* - a JWT secret shared between the services
*
* @example
* ```ts
* import { $serviceAccount } from "alepha/security";
*
* class MyService {
* serviceAccount = $serviceAccount({
* oauth2: {
* url: "https://example.com/oauth2/token",
* clientId: "your-client-id",
* clientSecret: "your-client-secret",
* }
* });
*
* async fetchData() {
* const token = await this.serviceAccount.token();
* // or
* const response = await this.serviceAccount.fetch("https://api.example.com/data");
* }
* }
* ```
*/
const $serviceAccount = (options) => {
const { context } = $cursor();
const store = {};
const dateTimeProvider = context.inject(DateTimeProvider);
const gracePeriod = options.gracePeriod ?? 30;
const cacheToken = (response) => {
store.cache = {
...response,
issued_at: dateTimeProvider.now().unix()
};
};
const getTokenFromCache = () => {
if (store.cache) {
const { access_token, expires_in, issued_at } = store.cache;
if (!expires_in) return access_token;
const now = dateTimeProvider.now().unix();
const expires = issued_at + expires_in;
if (expires - gracePeriod > now) return access_token;
}
};
if ("oauth2" in options) {
const { url, clientId, clientSecret } = options.oauth2;
const token = async () => {
const tokenFromCache = getTokenFromCache();
if (tokenFromCache) return tokenFromCache;
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: clientId,
client_secret: clientSecret
})
});
const json = await response.json();
if (!json.access_token || !json.expires_in) throw new Error(`Failed to fetch access token: ${JSON.stringify(json)}`);
cacheToken(json);
return json.access_token;
};
return { token };
}
return { token: async () => {
const tokenFromCache = getTokenFromCache();
if (tokenFromCache) return tokenFromCache;
const token = await options.realm.createToken(options.user);
cacheToken({
...token,
issued_at: dateTimeProvider.now().unix()
});
return token.access_token;
} };
};
//#endregion
//#region src/schemas/permissionSchema.ts
const permissionSchema = t.object({
name: t.string({ description: "Name of the permission." }),
group: t.optional(t.string({ description: "Group of the permission." })),
description: t.optional(t.string({ description: "Describe the permission." })),
method: t.optional(t.string({ description: "HTTP method of the permission. When available." })),
path: t.optional(t.string({ description: "Pathname of the permission. When available." }))
});
//#endregion
//#region src/schemas/roleSchema.ts
const roleSchema = t.object({
name: t.string({ description: "Name of the role." }),
description: t.optional(t.string({ description: "Describe the role." })),
default: t.optional(t.boolean({ description: "If true, this role will be assigned to all users by default." })),
permissions: t.array(t.object({
name: t.string({ description: "Name of the permission." }),
ownership: t.optional(t.boolean({ description: "If true, user will only have access to it's own resources." })),
exclude: t.optional(t.array(t.string(), { description: "Exclude some permissions. Useful when 'name' is a wildcard." }))
}))
});
//#endregion
//#region src/schemas/userAccountInfoSchema.ts
const userAccountInfoSchema = t.object({
id: t.string({ description: "Unique identifier for the user." }),
name: t.optional(t.string({ description: "Full name of the user." })),
email: t.optional(t.string({
description: "Email address of the user.",
format: "email"
})),
username: t.optional(t.string({ description: "Preferred username of the user." })),
picture: t.optional(t.string({ description: "URL to the user's profile picture." })),
sessionId: t.optional(t.string({ description: "Session identifier for the user, if applicable." })),
organizations: t.optional(t.array(t.string(), { description: "List of organizations the user belongs to." })),
roles: t.optional(t.array(t.string(), { description: "List of roles assigned to the user." }))
});
//#endregion
//#region src/index.ts
/**
* Provides comprehensive authentication and authorization capabilities with JWT tokens, role-based access control, and user management.
*
* The security module enables building secure applications using descriptors like `$realm`, `$role`, and `$permission`
* on class properties. It offers JWT-based authentication, fine-grained permissions, service accounts, and seamless
* integration with various authentication providers and user management systems.
*
* @see {@link $realm}
* @see {@link $role}
* @see {@link $permission}
* @module alepha.security
*/
const AlephaSecurity = $module({
name: "alepha.security",
descriptors: [
$realm,
$role,
$permission
],
services: [
SecurityProvider,
JwtProvider,
CryptoProvider
]
});
//#endregion
export { $permission, $realm, $role, $serviceAccount, AlephaSecurity, CryptoProvider, InvalidPermissionError, JwtProvider, PermissionDescriptor, RealmDescriptor, RoleDescriptor, SecurityError, SecurityProvider, permissionSchema, roleSchema, userAccountInfoSchema };
//# sourceMappingURL=index.js.map