UNPKG

@alepha/security

Version:

Manage realms, roles, permissions, and JWT-based authentication.

877 lines (861 loc) 28.7 kB
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