Laqus.Platform.Auth
2.8.1
dotnet add package Laqus.Platform.Auth --version 2.8.1
NuGet\Install-Package Laqus.Platform.Auth -Version 2.8.1
<PackageReference Include="Laqus.Platform.Auth" Version="2.8.1" />
<PackageVersion Include="Laqus.Platform.Auth" Version="2.8.1" />
<PackageReference Include="Laqus.Platform.Auth" />
paket add Laqus.Platform.Auth --version 2.8.1
#r "nuget: Laqus.Platform.Auth, 2.8.1"
#:package Laqus.Platform.Auth@2.8.1
#addin nuget:?package=Laqus.Platform.Auth&version=2.8.1
#tool nuget:?package=Laqus.Platform.Auth&version=2.8.1
Laqus Platform Auth
Pacote NuGet para integração de serviços com o sistema de autenticação e autorização da plataforma Laqus.
Setup
Configuração (appsettings)
"LaqusAuth": {
"AuthMiddlewareApiUrl": "http://localhost:3030/",
"Cache": {
"Ttl": 3600,
"EnableTls": false,
"RedisUser": "",
"RedisPassword": "",
"RedisHost": "localhost",
"RedisPort": 6379,
"AuthCacheKeysPrefix": ""
},
"Keycloak": {
"Authority": "http://localhost:9990/realms/laqus-platform",
"ClientId": "my-tenant-api-client",
"Issuer": "http://localhost:9990/realms/laqus-platform",
"ClientSecret": "********************"
}
}
Registro dos Serviços
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddLaqusAuth(builder.Configuration, option =>
{
option.AppName = "App Teste";
option.AuthMiddlewareApiUrl = builder.Configuration["LaqusAuth:AuthMiddlewareApiUrl"];
option.Options = new AuthOptions
{
Authority = builder.Configuration["LaqusAuth:Keycloak:Authority"],
ClientId = builder.Configuration["LaqusAuth:Keycloak:ClientId"],
ClientSecret = builder.Configuration["LaqusAuth:Keycloak:ClientSecret"],
Issuer = builder.Configuration["LaqusAuth:Keycloak:Issuer"],
};
option.CacheOptions = new CacheOptions
{
Ttl = int.Parse(builder.Configuration["LaqusAuth:Cache:Ttl"] ?? "3600"),
EnableTls = builder.Configuration["LaqusAuth:Cache:EnableTls"] == "true",
RedisUser = builder.Configuration["LaqusAuth:Cache:RedisUser"],
RedisPassword = builder.Configuration["LaqusAuth:Cache:RedisPassword"],
RedisHost = builder.Configuration["LaqusAuth:Cache:RedisHost"],
RedisPort = int.Parse(builder.Configuration["LaqusAuth:Cache:RedisPort"]!),
AuthCacheKeysPrefix = builder.Configuration["LaqusAuth:Cache:AuthCacheKeysPrefix"]
};
});
Pipeline de Middlewares
var app = builder.Build();
app.UseRouting();
app.UseAuthentication();
app.UseLaqusLegacySSOAuthenticationMiddleware();
app.UseAuthorization();
// Opção 1: Autorização completa + tratamento de erros JWT
app.UseLaqusAuthorization();
// Opção 2: Apenas autorização (sem tratamento de erros JWT)
app.UseLaqusAuthorizationMiddleware();
// Opção 3: Apenas tratamento de erros JWT (sem autorização)
app.UseLaqusJwtErrorHandler();
app.MapControllers();
Usuário Autenticado
Acesse informações do usuário logado via IPlatformAuthService:
public class MyController : ControllerBase
{
private readonly IPlatformAuthService _platformAuthService;
public MyController(IPlatformAuthService platformAuthService)
{
_platformAuthService = platformAuthService;
}
[HttpGet("current-user")]
[IsAuthenticated]
public IActionResult GetCurrentUser()
{
if (!_platformAuthService.IsUserAuthenticated)
return Unauthorized();
return Ok(new
{
Token = _platformAuthService.TokenInfo.Token,
User = _platformAuthService.AuthenticatedUser
});
}
}
Autorização por Actions
Actions são permissões granulares baseadas em LRN (Laqus Resource Name) e operações.
- LRN:
lrn:laqus:v1::<domínio>/<subdomínio>/<...>/<subdomínio>:<recurso> - Action: Operação específica (
criar,consultar,atualizar,excluir)
As actions estão centralizadas na classe estática PlatformActions:
using Laqus.Platform.Auth.Authorization;
PlatformActions.Securitizacao.Termos.Criar
PlatformActions.Securitizacao.Termos.Consultar
PlatformActions.Securitizacao.Termos.Atualizar
PlatformActions.Securitizacao.Termos.Excluir
PlatformActions.Securitizacao.Termos.ConsultarGlobal
PlatformActions.Securitizacao.Termos.Anexos.Criar
PlatformActions.Escrituracao.Auditoria.Logs.Consultar
Decorators
[RequireAction] — Protegendo Endpoints
using Laqus.Platform.Auth.Authorization;
using Laqus.Platform.Auth.Decorators;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class CadastrosController : ControllerBase
{
[HttpGet]
[RequireAction(PlatformActions.Cadastro.Cadastros.ListBase)]
public async Task<IActionResult> ListCadastros()
{
return Ok(await GetCadastros());
}
}
Múltiplas Actions (AND)
Por padrão, todas as actions devem ser satisfeitas (estratégia ALL):
[HttpPost("cadastros/{id}/approve")]
[RequireAction(
PlatformActions.Cadastro.Cadastros.Update,
PlatformActions.Cadastro.Cadastros.Administrar
)]
public async Task<IActionResult> ApproveCadastro(Guid id)
{
return Ok();
}
Controller vs Método
O atributo pode ser aplicado no controller (afeta todos os métodos) e sobrescrito por método:
[ApiController]
[Route("api/[controller]")]
[RequireAction(PlatformActions.Cadastro.Cadastros.GetBase)]
public class CadastrosController : ControllerBase
{
[HttpGet]
public async Task<IActionResult> List()
{
// Usa a action do controller: GetBase
return Ok();
}
[HttpPost]
[RequireAction(PlatformActions.Cadastro.Cadastros.Create)]
public async Task<IActionResult> Create()
{
// Sobrescreve com a action do método: Create
return Ok();
}
}
[IsPublic] e [IsAuthenticated]
[HttpPost("login")]
[IsPublic]
public async Task<IActionResult> Login([FromBody] LoginDto dto)
{
// Endpoint público, sem autenticação
return Ok();
}
[HttpGet("profile")]
[IsAuthenticated]
public async Task<IActionResult> GetProfile()
{
// Requer apenas autenticação, sem validação de permissões
return Ok();
}
Validação Programática
Para lógica condicional complexa, use IPlatformAuthorizationService:
using Laqus.Platform.Auth.LaqusAuthMiddleware.Service;
using Laqus.Platform.Auth.Authorization;
public class CadastrosApplicationService
{
private readonly IPlatformAuthorizationService _authorizationService;
public CadastrosApplicationService(
IPlatformAuthorizationService authorizationService)
{
_authorizationService = authorizationService;
}
}
Action Única
public async Task<IActionResult> ProcessarCadastro(Guid cadastroId)
{
var hasPermission = await _authorizationService.AuthorizeAsync(
PlatformActions.Cadastro.Cadastros.Update
);
if (!hasPermission)
throw new ForbiddenException("Usuário sem permissão para atualizar cadastros");
// Continua processamento...
}
Múltiplas Actions (ALL / ANY)
// Estratégia ALL — todas as actions são necessárias
var hasAll = await _authorizationService.AuthorizeAsync(
new[]
{
PlatformActions.Cadastro.Cadastros.Update,
PlatformActions.Cadastro.Cadastros.Administrar
},
AuthorizationStrategy.ALL
);
// Estratégia ANY — pelo menos uma das actions
var hasAny = await _authorizationService.AuthorizeAsync(
new[]
{
PlatformActions.Cadastro.Cadastros.GetBase,
PlatformActions.Cadastro.Cadastros.GetDetails
},
AuthorizationStrategy.ANY
);
Validação de User Type
using Laqus.Platform.Auth.LaqusAuthMiddleware.Models.Authorization;
var isServiceAccount = await _authorizationService.AuthorizeApiServiceAccountAsync();
var isRegularUser = await _authorizationService.AuthorizeRegularUserAsync();
Exemplo: Resposta Condicional por Permissão
public async Task<CadastroDto> GetCadastro(Guid cadastroId)
{
var cadastro = await _cadastroService.GetByIdAsync(cadastroId);
var dto = _mapper.Map<CadastroDto>(cadastro);
var canViewSensitiveData = await _authorizationService.AuthorizeAsync(
PlatformActions.Cadastro.Cadastros.GetDetails
);
if (!canViewSensitiveData)
{
dto.DadosBancarios = null;
dto.DocumentosPessoais = null;
}
return dto;
}
Gerenciamento de Políticas
Use IAuthMiddlewareUsersProxy (registrado automaticamente por AddLaqusAuth) para gerenciar usuários e permissões via código.
Actions = Permissões granulares por recurso | Policies = Agrupamentos de actions (ex: "Admin", "Reader")
public class UserPermissionService
{
private readonly IAuthMiddlewareUsersProxy _usersProxy;
public UserPermissionService(IAuthMiddlewareUsersProxy usersProxy)
{
_usersProxy = usersProxy;
}
}
Atribuir Políticas
using Laqus.Platform.Auth.Authorization;
using Laqus.Platform.Auth.LaqusAuthMiddleware.Proxy.Users.Dtos.Request;
// Por nome
var assignDto = new AssignUserPoliciesDto
{
PoliciesNames = new[]
{
PlatformPolicies.AccountManagement.User.Admin,
PlatformPolicies.Cadastro.CadastrosEditor
}
};
await _usersProxy.AssignPoliciesToUserAsync(userId, assignDto);
// Por ID
var assignById = new AssignUserPoliciesDto { PoliciesIds = policyIds };
await _usersProxy.AssignPoliciesToUserAsync(userId, assignById);
// Via grupo
var assignViaGroup = new AssignUserPoliciesDto
{
GroupId = groupId,
PoliciesNames = new[] { PlatformPolicies.Cadastro.CadastrosReader }
};
await _usersProxy.AssignPoliciesToUserAsync(userId, assignViaGroup);
Consultar e Remover Políticas
// Consultar
var response = await _usersProxy.GetUserPoliciesAsync(userId);
if (!response.Success)
throw new InvalidOperationException($"Falha ao buscar políticas: {response.Message}");
var policies = response.Result;
// Remover
using Laqus.Platform.Auth.LaqusAuthMiddleware.Proxy.Users.Dtos.Request;
var removeDto = new RemovePoliciesFromUserDto
{
PoliciesNames = new[] { PlatformPolicies.Cadastro.CadastrosEditor }
};
await _usersProxy.RemovePoliciesFromUserAsync(userId, removeDto);
Gerenciamento de Status
await _usersProxy.SetUserStatusAsync(userId, active: true);
await _usersProxy.SetUserStatusAsync(userId, active: false);
Exemplo Completo — Onboarding
public class UserOnboardingService
{
private readonly IAuthMiddlewareUsersProxy _usersProxy;
private readonly ILogger<UserOnboardingService> _logger;
public UserOnboardingService(
IAuthMiddlewareUsersProxy usersProxy,
ILogger<UserOnboardingService> logger)
{
_usersProxy = usersProxy;
_logger = logger;
}
public async Task<Guid> CreateUserWithBasicPermissions(
string email, string firstName, string lastName)
{
_logger.LogInformation("Creating user {Email}", email);
var createResponse = await _usersProxy.CreateUserAsync(new CreateUserDto
{
Email = email, FirstName = firstName,
LastName = lastName, Enabled = true
});
if (!createResponse.Success)
throw new InvalidOperationException(
$"Falha ao criar usuário: {createResponse.Message}");
var userId = createResponse.Result.Id;
await _usersProxy.AssignPoliciesToUserAsync(userId, new AssignUserPoliciesDto
{
PoliciesNames = new[]
{
PlatformPolicies.Cadastro.CadastrosReader,
PlatformPolicies.Cadastro.DocumentosReader
}
});
_logger.LogInformation("User {UserId} created with basic permissions", userId);
return userId;
}
public async Task PromoteToEditor(Guid userId)
{
await _usersProxy.AssignPoliciesToUserAsync(userId, new AssignUserPoliciesDto
{
PoliciesNames = new[]
{
PlatformPolicies.Cadastro.CadastrosEditor,
PlatformPolicies.Cadastro.DocumentosEditor
}
});
_logger.LogInformation("User {UserId} promoted to editor", userId);
}
public async Task RevokeEditorPermissions(Guid userId)
{
await _usersProxy.RemovePoliciesFromUserAsync(userId, new RemovePoliciesFromUserDto
{
PoliciesNames = new[]
{
PlatformPolicies.Cadastro.CadastrosEditor,
PlatformPolicies.Cadastro.DocumentosEditor
}
});
_logger.LogInformation("Editor permissions revoked from user {UserId}", userId);
}
}
Tratamento de Erros e Personalização
Respostas de Erro
A biblioteca retorna JSON estruturado com os códigos HTTP apropriados:
| HTTP Status | Código | Quando |
|---|---|---|
| 401 Unauthorized | AUTH_REQUIRED |
Usuário não autenticado |
| 401 Unauthorized | TOKEN_INVALID |
Token JWT inválido |
| 401 Unauthorized | TOKEN_EXPIRED |
Token JWT expirado |
| 403 Forbidden | INSUFFICIENT_PERMISSIONS |
Permissões insuficientes |
| 403 Forbidden | ROLE_REQUIRED |
Função específica necessária |
| 403 Forbidden | ACTION_REQUIRED |
Ação específica necessária |
| 403 Forbidden | USER_TYPE_REQUIRED |
Tipo de usuário incompatível |
Formato da resposta:
{
"codigo": "AUTH_REQUIRED",
"mensagem": "Autenticação necessária",
"detalhes": "Esta operação requer que você esteja autenticado."
}
Personalização de Mensagens
builder.Services.AddLaqusAuthorization(options =>
{
options.ErrorMessages.AuthenticationRequired = "Autenticação necessária";
options.ErrorMessages.AuthenticationRequiredDetails = "Você precisa estar logado para acessar este recurso.";
options.ErrorMessages.InsufficientPermissions = "Permissão negada";
options.ErrorMessages.ActionRequired = "Permissão insuficiente";
options.ErrorMessages.TokenInvalid = "Token inválido";
options.ErrorMessages.TokenInvalidDetails = "O token fornecido é inválido ou mal-formado.";
options.ErrorMessages.TokenExpired = "Sua sessão expirou";
options.ErrorMessages.TokenExpiredDetails = "Por favor, faça login novamente.";
});
Todas as propriedades personalizáveis:
| Propriedade | Descrição |
|---|---|
AuthenticationRequired / AuthenticationRequiredDetails |
Autenticação necessária |
InsufficientPermissions / InsufficientPermissionsDetails |
Permissões insuficientes |
RoleRequired / RoleRequiredDetails |
Função necessária |
ActionRequired / ActionRequiredDetails |
Ação necessária |
UserTypeRequired / UserTypeRequiredDetails |
Tipo de usuário incompatível |
TokenInvalid / TokenInvalidDetails |
Token JWT inválido |
TokenExpired / TokenExpiredDetails |
Token JWT expirado |
🔐 Redis ACL (Redis 6+ / Valkey)
A partir da versão 2.8.0, a lib suporta autenticação Redis via ACL (AUTH username password).
🆕 Propriedade RedisUser
| Propriedade | Tipo | Obrigatória | Descrição |
|---|---|---|---|
RedisUser |
string |
❌ Não | Username para autenticação ACL no Redis/Valkey |
🧩 Como funciona
CacheOptions.RedisUser
↓
connection string: ,user=<username>
↓
ConfigurationOptions.Parse(...)
↓
ConfigurationOptions.User
↓
Redis/Valkey: AUTH <username> <password>
📝 Exemplo de configuração
"Cache": {
"RedisUser": "laqus-auth",
"RedisPassword": "my-secret",
"RedisHost": "valkey.internal",
"RedisPort": 6379
}
✅ Retrocompatibilidade
RedisUser = nullou""→ comportamento idêntico ao anterior (semuser=na connection string)- Nenhuma alteração necessária para consumidores existentes
⚠️ Pontos de Atenção
- LRN Case-Sensitive — Use exatamente como definido em
PlatformActions. - Decorators vs Programática — Decorators para proteção de rotas; programática para lógica condicional complexa.
- Validação em Runtime — Ocorre no middleware, antes do controller. Sem overhead no método.
- Cache (Redis) — Permissões são cacheadas. Configure o TTL adequado e considere-o ao validar alterações recentes.
- Performance —
AuthorizeAsyncconsulta cache, mas evite chamadas excessivas em loops. - Idempotência — Atribuir a mesma política múltiplas vezes não gera erro.
- Transações — Operações de atribuição/remoção de políticas são individuais. Implemente compensação se necessário.
- Permissões para gerenciar políticas — O usuário precisa de
PlatformActions.AccountManagement.User.Policies.Assigne.Unassign. - Exception Handling — Lance exceções específicas (
ForbiddenException,UnauthorizedException). TrateHttpRequestExceptionao comunicar com o Auth Middleware. - Testing — Use tokens JWT válidos ou mocke
IPlatformAuthService. - Logging — Sempre logue operações de atribuição/remoção de permissões para auditoria.
Checklist Rápido
Autorização por Actions:
-
using Laqus.Platform.Auth.Authorization;eusing Laqus.Platform.Auth.Decorators; -
[RequireAction(PlatformActions...)]nos endpoints -
[IsAuthenticated]ou[IsPublic]onde aplicável
Gerenciamento de Políticas:
- Injetar
IAuthMiddlewareUsersProxy - Usar constantes de
PlatformPolicies - Tratamento de erros e logging
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 is compatible. net5.0-windows was computed. net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 is compatible. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 is compatible. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETCoreApp 3.1
- Microsoft.IdentityModel.JsonWebTokens (>= 7.5.1)
- Microsoft.IdentityModel.Protocols.OpenIdConnect (>= 7.5.1)
- Polly (>= 7.2.4)
- StackExchange.Redis (>= 2.6.122)
- System.IdentityModel.Tokens.Jwt (>= 7.5.1)
- System.Net.Http.Json (>= 6.0.2)
-
.NETStandard 2.0
- Microsoft.AspNetCore.Http (>= 2.2.2)
- Microsoft.AspNetCore.Http.Abstractions (>= 2.2.0)
- Microsoft.AspNetCore.Http.Extensions (>= 2.1.21)
- Microsoft.Bcl.AsyncInterfaces (>= 8.0.0)
- Microsoft.Extensions.Caching.Memory (>= 2.2.0)
- Microsoft.Extensions.DependencyInjection (>= 2.1.1)
- Microsoft.Extensions.Hosting.Abstractions (>= 2.1.1)
- Microsoft.Extensions.Http (>= 2.1.1)
- Microsoft.Extensions.Logging (>= 2.1.1)
- Microsoft.Extensions.Logging.Configuration (>= 2.1.1)
- Microsoft.Extensions.Logging.Console (>= 2.1.1)
- Microsoft.IdentityModel.JsonWebTokens (>= 7.5.1)
- Microsoft.IdentityModel.Protocols.OpenIdConnect (>= 7.5.1)
- Polly (>= 7.2.4)
- StackExchange.Redis (>= 2.6.122)
- System.ComponentModel.Annotations (>= 4.7.0)
- System.IdentityModel.Tokens.Jwt (>= 7.5.1)
- System.Net.Http.Json (>= 6.0.2)
- System.Security.Cryptography.Cng (>= 4.7.0)
- System.Text.Json (>= 8.0.5)
- System.Threading.Channels (>= 5.0.0)
-
.NETStandard 2.1
- Microsoft.AspNetCore.Http (>= 2.2.2)
- Microsoft.AspNetCore.Http.Abstractions (>= 2.2.0)
- Microsoft.Extensions.Caching.Memory (>= 2.2.0)
- Microsoft.Extensions.DependencyInjection (>= 3.0.3)
- Microsoft.Extensions.Hosting.Abstractions (>= 3.0.3)
- Microsoft.Extensions.Http (>= 3.0.3)
- Microsoft.Extensions.Logging (>= 3.0.3)
- Microsoft.Extensions.Logging.Configuration (>= 3.0.3)
- Microsoft.Extensions.Logging.Console (>= 3.0.3)
- Microsoft.IdentityModel.JsonWebTokens (>= 7.5.1)
- Microsoft.IdentityModel.Protocols.OpenIdConnect (>= 7.5.1)
- Polly (>= 7.2.4)
- StackExchange.Redis (>= 2.6.122)
- System.IdentityModel.Tokens.Jwt (>= 7.5.1)
- System.Net.Http.Json (>= 6.0.2)
-
net10.0
- Microsoft.IdentityModel.JsonWebTokens (>= 7.5.1)
- Microsoft.IdentityModel.Protocols.OpenIdConnect (>= 7.5.1)
- Polly (>= 8.0.0)
- StackExchange.Redis (>= 2.6.122)
- System.IdentityModel.Tokens.Jwt (>= 7.5.1)
-
net5.0
- Microsoft.IdentityModel.JsonWebTokens (>= 7.5.1)
- Microsoft.IdentityModel.Protocols.OpenIdConnect (>= 7.5.1)
- Polly (>= 7.2.4)
- StackExchange.Redis (>= 2.6.122)
- System.IdentityModel.Tokens.Jwt (>= 7.5.1)
-
net6.0
- Microsoft.IdentityModel.JsonWebTokens (>= 7.5.1)
- Microsoft.IdentityModel.Protocols.OpenIdConnect (>= 7.5.1)
- Polly (>= 8.0.0)
- StackExchange.Redis (>= 2.6.122)
- System.IdentityModel.Tokens.Jwt (>= 7.5.1)
-
net7.0
- Microsoft.IdentityModel.JsonWebTokens (>= 7.5.1)
- Microsoft.IdentityModel.Protocols.OpenIdConnect (>= 7.5.1)
- Polly (>= 8.0.0)
- StackExchange.Redis (>= 2.6.122)
- System.IdentityModel.Tokens.Jwt (>= 7.5.1)
-
net8.0
- Microsoft.IdentityModel.JsonWebTokens (>= 7.5.1)
- Microsoft.IdentityModel.Protocols.OpenIdConnect (>= 7.5.1)
- Polly (>= 8.0.0)
- StackExchange.Redis (>= 2.6.122)
- System.IdentityModel.Tokens.Jwt (>= 7.5.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.8.1 | 114 | 8/24/2026 |
| 2.8.0 | 151 | 8/14/2026 |
| 2.7.1 | 98 | 8/12/2026 |
| 2.7.0 | 109 | 7/31/2026 |
| 2.6.0 | 104 | 7/28/2026 |
| 2.5.0 | 253 | 7/20/2026 |
| 2.4.0 | 102 | 7/16/2026 |
| 2.3.0 | 234 | 7/1/2026 |
| 2.2.2 | 282 | 6/15/2026 |
| 2.2.1 | 111 | 6/12/2026 |
| 2.2.0 | 117 | 6/2/2026 |
| 2.1.3 | 118 | 6/1/2026 |
| 2.1.2 | 173 | 5/13/2026 |
| 2.1.1 | 243 | 4/14/2026 |
| 2.1.0 | 111 | 4/14/2026 |
| 2.0.1 | 112 | 4/13/2026 |
| 2.0.0 | 141 | 4/7/2026 |
| 1.1.0 | 198 | 3/10/2026 |
| 1.0.18 | 994 | 10/14/2025 |
| 1.0.17 | 261 | 9/23/2025 |