diff --git a/dist/index.mjs b/dist/index.mjs
index 9b6a5d94a72cf1ad13235c1f76ca5f57d4380d2e..7c9d0e8bbd4e8a522f2ab4db5d7424a7dcb6bf31 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -439,2 +439,12 @@ async function createOpaqueAccessToken(ctx, opts, user, client, scopes, payload,
 }
+const REFRESH_TOKEN_ROTATION_GRACE_MS = 30_000;
+// Only token hashes are stored, so an overlap replay can issue another
+// successor instead of returning prior plaintext. Stale replays still
+// invalidate the family; successors remain normal rotating grants.
+function isRefreshTokenRotationWithinGrace(refreshToken) {
+	const revokedAt = normalizeTimestampValue(refreshToken.revoked);
+	if (!revokedAt) return false;
+	const revokedAgeMs = Date.now() - revokedAt.getTime();
+	return revokedAgeMs >= 0 && revokedAgeMs <= REFRESH_TOKEN_ROTATION_GRACE_MS;
+}
 /**
@@ -521,11 +531,32 @@ async function createRefreshToken(ctx, opts, user, referenceId, client, scopes, p
-	})) throw new APIError("BAD_REQUEST", {
-		error_description: "invalid refresh token",
-		error: "invalid_grant"
-	});
-	return {
-		id: (await ctx.context.adapter.create({
-			model: "oauthRefreshToken",
-			data: newRow
-		})).id,
-		token: await encodeRefreshToken(opts, token, sessionId)
-	};
+	})) {
+		const racedRefresh = await ctx.context.adapter.findOne({
+			model: "oauthRefreshToken",
+			where: [{
+				field: "id",
+				value: originalRefresh.id
+			}]
+		});
+		if (!racedRefresh || !isRefreshTokenRotationWithinGrace(racedRefresh)) {
+			await invalidateRefreshFamily(ctx, client.clientId, user.id);
+			throw new APIError("BAD_REQUEST", {
+				error_description: "invalid refresh token",
+				error: "invalid_grant"
+			});
+		}
+	}
+	const replacement = await ctx.context.adapter.create({
+		model: "oauthRefreshToken",
+		data: newRow
+	});
+	const originalRefreshStillPresent = await ctx.context.adapter.findOne({
+		model: "oauthRefreshToken",
+		where: [{
+			field: "id",
+			value: originalRefresh.id
+		}]
+	});
+	if (!originalRefreshStillPresent) await invalidateRefreshFamily(ctx, client.clientId, user.id);
+	return {
+		id: replacement.id,
+		token: await encodeRefreshToken(opts, token, sessionId)
+	};
@@ -842 +873,2 @@ async function handleRefreshTokenGrant(ctx, opts) {
-	if (refreshToken.revoked) {
+	const replayedWithinRotationGrace = Boolean(refreshToken.revoked) && isRefreshTokenRotationWithinGrace(refreshToken);
+	if (refreshToken.revoked && !replayedWithinRotationGrace) {
