diff --git a/src/api/SIGCM2.Domain/Entities/RefreshToken.cs b/src/api/SIGCM2.Domain/Entities/RefreshToken.cs
new file mode 100644
index 0000000..c62353e
--- /dev/null
+++ b/src/api/SIGCM2.Domain/Entities/RefreshToken.cs
@@ -0,0 +1,72 @@
+namespace SIGCM2.Domain.Entities;
+
+public sealed class RefreshToken
+{
+ public int Id { get; init; }
+ public int UsuarioId { get; init; }
+ public string TokenHash { get; init; } = null!;
+ public Guid FamilyId { get; init; }
+ public DateTime IssuedAt { get; init; }
+ public DateTime ExpiresAt { get; init; }
+ public DateTime? RevokedAt { get; private set; }
+ public int? ReplacedById { get; private set; }
+ public string CreatedByIp { get; init; } = null!;
+ public string? UserAgent { get; init; }
+
+ /// Factory for a brand-new session (login). Generates a new FamilyId.
+ public static RefreshToken IssueForNewFamily(
+ int usuarioId,
+ string tokenHash,
+ DateTime now,
+ TimeSpan ttl,
+ string createdByIp,
+ string? userAgent)
+ => new()
+ {
+ UsuarioId = usuarioId,
+ TokenHash = tokenHash,
+ FamilyId = Guid.NewGuid(),
+ IssuedAt = now,
+ ExpiresAt = now + ttl,
+ CreatedByIp = createdByIp,
+ UserAgent = userAgent,
+ };
+
+ /// Factory for a rotation. Inherits FamilyId and ExpiresAt (absolute TTL).
+ public static RefreshToken IssueRotation(
+ RefreshToken previous,
+ string newTokenHash,
+ DateTime now,
+ string createdByIp,
+ string? userAgent)
+ => new()
+ {
+ UsuarioId = previous.UsuarioId,
+ TokenHash = newTokenHash,
+ FamilyId = previous.FamilyId,
+ IssuedAt = now,
+ ExpiresAt = previous.ExpiresAt, // ABSOLUTE — inherited from original
+ CreatedByIp = createdByIp,
+ UserAgent = userAgent,
+ };
+
+ /// Returns true if ExpiresAt <= now (expired).
+ public bool IsExpired(DateTime now) => now >= ExpiresAt;
+
+ /// Returns true if the token has been explicitly revoked.
+ public bool IsRevoked => RevokedAt.HasValue;
+
+ /// Returns true if the token is neither revoked nor expired.
+ public bool IsActive(DateTime now) => !IsRevoked && !IsExpired(now);
+
+ ///
+ /// Marks the token as revoked with the given timestamp and optional successor id.
+ /// Should only be called by the repository when reconstructing or persisting state.
+ /// This is NOT the way to revoke in business logic — use the repository SQL methods.
+ ///
+ public void MarkAsPersistedRevocation(DateTime revokedAt, int? replacedById)
+ {
+ RevokedAt = revokedAt;
+ ReplacedById = replacedById;
+ }
+}